> ## Documentation Index
> Fetch the complete documentation index at: https://docs.endstate.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Consume a tap intent

> Uses an approved tap intent, once, and sends `tap_intent.consumed`. Call it from your server before you perform the action the person approved, with the `action` you are about to perform: it must equal the action the tap intent was opened with. It succeeds for one request, until `use_by`. Send an `Idempotency-Key` you have stored: a retry with the same key and the same secret key returns the consumed tap intent again with `Idempotent-Replayed: true`, also after `use_by`. Any other request gets `tap_intent.already_consumed`, with `details.consumed_at`, and must not act.



## OpenAPI

````yaml /openapi.json post /v1/tap-intents/{tap_intent_id}/consume
openapi: 3.1.0
info:
  title: Endstate API
  version: 0.1.0
  description: Endstate developer API for chip verification and ownership workflows.
servers:
  - url: https://api2.endstate.io
    description: Production
  - url: https://api-staging.endstate.io
    description: Staging
security: []
paths:
  /v1/tap-intents/{tap_intent_id}/consume:
    post:
      tags:
        - Tap intents
      summary: Consume a tap intent
      description: >-
        Uses an approved tap intent, once, and sends `tap_intent.consumed`. Call
        it from your server before you perform the action the person approved,
        with the `action` you are about to perform: it must equal the action the
        tap intent was opened with. It succeeds for one request, until `use_by`.
        Send an `Idempotency-Key` you have stored: a retry with the same key and
        the same secret key returns the consumed tap intent again with
        `Idempotent-Replayed: true`, also after `use_by`. Any other request gets
        `tap_intent.already_consumed`, with `details.consumed_at`, and must not
        act.
      operationId: consumeTapIntent
      parameters:
        - schema:
            type: string
            pattern: ^tapi_[0-9a-f]{32}$
            example: tapi_3f9c1b7a2d4e4f60a1b2c3d4e5f60718
          required: true
          name: tap_intent_id
          in: path
        - schema:
            type: string
            minLength: 1
            maxLength: 255
            description: >-
              Unique key that makes this request safe to retry. Replaying the
              key with the same body returns the original response with
              `Idempotent-Replayed: true`; replaying it with a different body
              returns 409 `idempotency.key_conflict`.
            example: a1b2c3d4-0000-4000-8000-000000000000
          required: false
          description: >-
            Unique key that makes this request safe to retry. Replaying the key
            with the same body returns the original response with
            `Idempotent-Replayed: true`; replaying it with a different body
            returns 409 `idempotency.key_conflict`.
          name: Idempotency-Key
          in: header
      requestBody:
        required: true
        content:
          application/json:
            example:
              action:
                type: listing.confirm
                reference: listing_8271
            schema:
              $ref: '#/components/schemas/ConsumeTapIntentRequest'
      responses:
        '200':
          description: The consumed tap intent.
          content:
            application/json:
              example:
                id: tapi_3f9c1b7a2d4e4f60a1b2c3d4e5f60718
                status: consumed
                rule: own
                unit_id: 6f1d2c3b-4a59-4e68-9f70-8a1b2c3d4e5f
                action:
                  type: listing.confirm
                  reference: listing_8271
                redirect_url: https://app.example.com/listings/8271/confirm
                display:
                  title: Confirm your listing
                code_required: true
                scan:
                  id: 0b8e6a54-93c2-4d17-8f5e-2a1b3c4d5e6f
                  scanned_at: '2026-11-10T17:03:40.000Z'
                created_at: '2026-11-10T17:02:11.000Z'
                expires_at: '2026-11-10T17:07:11.000Z'
                approved_at: '2026-11-10T17:04:02.000Z'
                use_by: '2026-11-10T17:09:02.000Z'
                consumed_at: '2026-11-10T17:04:05.000Z'
                canceled_at: null
              schema:
                $ref: '#/components/schemas/TapIntent'
        '400':
          description: >-
            The request was malformed or failed validation.


            | Error code | When |

            | --- | --- |

            | `tap_intent.action_mismatch` | `action` is not the action the tap
            intent was opened with. Send the action you are about to perform,
            with the same `type` and `reference` as at create. |

            | `validation.failed` | The request failed schema validation. See
            `error.details` for per-field issues. |
          x-error-codes:
            - tap_intent.action_mismatch
            - validation.failed
          content:
            application/json:
              examples:
                tap_intent.action_mismatch:
                  summary: >-
                    `action` is not the action the tap intent was opened with.
                    Send the action you are about to perform, with the same
                    `type` and `reference` as at create.
                  value:
                    error:
                      code: tap_intent.action_mismatch
                      message: >-
                        `action` is not the action the tap intent was opened
                        with. Send the action you are about to perform, with the
                        same `type` and `reference` as at create.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: >-
                        https://docs.endstate.io/errors/tap-intent-action-mismatch
                validation.failed:
                  summary: >-
                    The request failed schema validation. See `error.details`
                    for per-field issues.
                  value:
                    error:
                      code: validation.failed
                      message: >-
                        The request failed schema validation. See
                        `error.details` for per-field issues.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/validation-failed
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: |-
            Authentication failed.

            | Error code | When |
            | --- | --- |
            | `auth.unauthorized` | Credential is missing or malformed. |
          x-error-codes:
            - auth.unauthorized
          content:
            application/json:
              examples:
                auth.unauthorized:
                  summary: Credential is missing or malformed.
                  value:
                    error:
                      code: auth.unauthorized
                      message: Credential is missing or malformed.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/auth-unauthorized
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: >-
            Authenticated, but not permitted.


            | Error code | When |

            | --- | --- |

            | `auth.forbidden` | Credential is valid but does not have access to
            the requested resource or action. |
          x-error-codes:
            - auth.forbidden
          content:
            application/json:
              examples:
                auth.forbidden:
                  summary: >-
                    Credential is valid but does not have access to the
                    requested resource or action.
                  value:
                    error:
                      code: auth.forbidden
                      message: >-
                        Credential is valid but does not have access to the
                        requested resource or action.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/auth-forbidden
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: >-
            The resource does not exist within your organization.


            | Error code | When |

            | --- | --- |

            | `tap_intent.not_found` | No tap intent with that id exists for
            this credential's environment. |
          x-error-codes:
            - tap_intent.not_found
          content:
            application/json:
              examples:
                tap_intent.not_found:
                  summary: >-
                    No tap intent with that id exists for this credential's
                    environment.
                  value:
                    error:
                      code: tap_intent.not_found
                      message: >-
                        No tap intent with that id exists for this credential's
                        environment.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/tap-intent-not-found
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: >-
            The request conflicts with the resource's current state.


            | Error code | When |

            | --- | --- |

            | `idempotency.in_progress` | A request with this `Idempotency-Key`
            is already in progress. |

            | `idempotency.key_conflict` | The `Idempotency-Key` was previously
            used with a different request body. |

            | `tap_intent.already_consumed` | The tap intent was already
            consumed by another request. `details.consumed_at` says when. Do not
            act on it; open a new tap intent if you still need an approval. |

            | `tap_intent.canceled` | The tap intent was canceled, so it can no
            longer be approved or consumed. |

            | `tap_intent.expired` | The tap intent expired: nobody approved it
            in time, or it was approved and not consumed by `use_by`. Open a new
            one. |

            | `tap_intent.not_approved` | The person has not approved the tap
            intent yet, so there is nothing to consume. Wait for `approved`. |
          x-error-codes:
            - idempotency.in_progress
            - idempotency.key_conflict
            - tap_intent.already_consumed
            - tap_intent.canceled
            - tap_intent.expired
            - tap_intent.not_approved
          content:
            application/json:
              examples:
                idempotency.in_progress:
                  summary: >-
                    A request with this `Idempotency-Key` is already in
                    progress.
                  value:
                    error:
                      code: idempotency.in_progress
                      message: >-
                        A request with this `Idempotency-Key` is already in
                        progress.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/idempotency-in-progress
                idempotency.key_conflict:
                  summary: >-
                    The `Idempotency-Key` was previously used with a different
                    request body.
                  value:
                    error:
                      code: idempotency.key_conflict
                      message: >-
                        The `Idempotency-Key` was previously used with a
                        different request body.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/idempotency-key-conflict
                tap_intent.already_consumed:
                  summary: >-
                    The tap intent was already consumed by another request.
                    `details.consumed_at` says when. Do not act on it; open a
                    new tap intent if you still need an approval.
                  value:
                    error:
                      code: tap_intent.already_consumed
                      message: >-
                        The tap intent was already consumed by another request.
                        `details.consumed_at` says when. Do not act on it; open
                        a new tap intent if you still need an approval.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: >-
                        https://docs.endstate.io/errors/tap-intent-already-consumed
                tap_intent.canceled:
                  summary: >-
                    The tap intent was canceled, so it can no longer be approved
                    or consumed.
                  value:
                    error:
                      code: tap_intent.canceled
                      message: >-
                        The tap intent was canceled, so it can no longer be
                        approved or consumed.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/tap-intent-canceled
                tap_intent.expired:
                  summary: >-
                    The tap intent expired: nobody approved it in time, or it
                    was approved and not consumed by `use_by`. Open a new one.
                  value:
                    error:
                      code: tap_intent.expired
                      message: >-
                        The tap intent expired: nobody approved it in time, or
                        it was approved and not consumed by `use_by`. Open a new
                        one.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/tap-intent-expired
                tap_intent.not_approved:
                  summary: >-
                    The person has not approved the tap intent yet, so there is
                    nothing to consume. Wait for `approved`.
                  value:
                    error:
                      code: tap_intent.not_approved
                      message: >-
                        The person has not approved the tap intent yet, so there
                        is nothing to consume. Wait for `approved`.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/tap-intent-not-approved
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: |-
            Too many requests.

            | Error code | When |
            | --- | --- |
            | `rate_limit.exceeded` | Per-key rate limit exceeded. |
          x-error-codes:
            - rate_limit.exceeded
          content:
            application/json:
              examples:
                rate_limit.exceeded:
                  summary: Per-key rate limit exceeded.
                  value:
                    error:
                      code: rate_limit.exceeded
                      message: Per-key rate limit exceeded.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/rate-limit-exceeded
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: >-
            Something went wrong on our end.


            | Error code | When |

            | --- | --- |

            | `internal.error` | An unexpected server error occurred. Retry with
            exponential backoff and include `request_id` in any support request.
            |
          x-error-codes:
            - internal.error
          content:
            application/json:
              examples:
                internal.error:
                  summary: >-
                    An unexpected server error occurred. Retry with exponential
                    backoff and include `request_id` in any support request.
                  value:
                    error:
                      code: internal.error
                      message: >-
                        An unexpected server error occurred. Retry with
                        exponential backoff and include `request_id` in any
                        support request.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/internal-error
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            The service cannot do this right now. See the error code.


            | Error code | When |

            | --- | --- |

            | `tap_intent.busy` | The tap intent is being changed by another
            request. Retry. |
          x-error-codes:
            - tap_intent.busy
          content:
            application/json:
              examples:
                tap_intent.busy:
                  summary: The tap intent is being changed by another request. Retry.
                  value:
                    error:
                      code: tap_intent.busy
                      message: >-
                        The tap intent is being changed by another request.
                        Retry.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/tap-intent-busy
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - ApiKeyBearer: []
components:
  schemas:
    ConsumeTapIntentRequest:
      type: object
      properties:
        action:
          type: object
          properties:
            type:
              type: string
              minLength: 1
              maxLength: 64
              description: What the tap approves, in your own vocabulary.
              example: listing.confirm
            reference:
              type: string
              minLength: 1
              maxLength: 255
              description: Your identifier for the thing being approved.
              example: listing_8271
          required:
            - type
            - reference
          description: >-
            The action you are about to perform, from your own state. It must
            equal the action the tap intent was opened with: after the same
            trimming as at create, `type` and `reference` are compared byte for
            byte.
      required:
        - action
      additionalProperties: false
    TapIntent:
      type: object
      properties:
        id:
          type: string
        status:
          type: string
          enum:
            - pending
            - approved
            - consumed
            - canceled
            - expired
          description: >-
            `pending` until the person confirms. `approved` once they have:
            consume it by `use_by`. `consumed` once you have used the approval;
            it is used once. `canceled` ended without an approval. `expired`
            means nobody approved in time, or the approval was not consumed by
            `use_by`. `consumed` and `canceled` are final. Approving, canceling
            and consuming each send a webhook; expiring sends none.
        rule:
          type: string
          enum:
            - own
        unit_id:
          type: string
          format: uuid
        action:
          type: object
          properties:
            type:
              type: string
            reference:
              type: string
          required:
            - type
            - reference
        redirect_url:
          type:
            - string
            - 'null'
        display:
          type: object
          properties:
            title:
              type:
                - string
                - 'null'
          required:
            - title
        code_required:
          type: boolean
          description: Whether the person must enter the code to confirm.
        scan:
          type:
            - object
            - 'null'
          properties:
            id:
              type: string
              format: uuid
            scanned_at:
              type: string
              format: date-time
          required:
            - id
            - scanned_at
          description: The tap the intent was approved with. Null until it is approved.
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
          description: >-
            A tap must arrive by this time. The person can confirm for 10
            minutes after their tap.
        approved_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When the person approved. Kept on an approval that expired unused.
        use_by:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            The approval can be consumed until this time: `approved_at` plus the
            `use_within` set at create. Null until approved.
        consumed_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When you consumed the approval. Null until then.
        canceled_at:
          type:
            - string
            - 'null'
          format: date-time
      required:
        - id
        - status
        - rule
        - unit_id
        - action
        - redirect_url
        - display
        - code_required
        - scan
        - created_at
        - expires_at
        - approved_at
        - use_by
        - consumed_at
        - canceled_at
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              anyOf:
                - $ref: '#/components/schemas/ErrorCode'
                - type: string
              description: >-
                Stable, namespaced error code. The `ErrorCode` catalogue lists
                every code defined today; handle an unrecognized value as a
                generic failure rather than throwing, since codes are added over
                time.
              example: chip.not_found
            message:
              type: string
              description: >-
                Human-readable description, for logs and debugging. Wording may
                change without notice - never parse or match against it.
              example: Chip not found
            request_id:
              type: string
              description: >-
                Identifier for this request, matching the `X-Request-Id`
                response header. Log it and include it in any support request.
              example: req_8e1a7f50-90ab-4cde-f012-3456789abcde
            doc_url:
              type: string
              description: Documentation page for this error code.
              example: https://docs.endstate.io/errors/chip-not-found
            details:
              $ref: '#/components/schemas/ValidationErrorDetails'
          required:
            - code
            - message
            - request_id
            - doc_url
      required:
        - error
      description: >-
        Every error response, regardless of endpoint or HTTP status, uses this
        envelope.
    ErrorCode:
      type: string
      enum:
        - environment.conflict
        - environment.invalid_configuration
        - organization.changed
        - validation.failed
        - auth.unauthorized
        - auth.forbidden
        - session_token.invalid_or_expired
        - session_token.wrong_chip
        - wallet_session.invalid_or_expired
        - not_found.resource
        - rate_limit.exceeded
        - chip.not_found
        - chip.invalid_e_value
        - chip.already_scanned
        - quota.exceeded
        - chip.not_a_test_chip
        - unit.not_found
        - unit.already_exists
        - unit.not_minted
        - collection.not_found
        - collection.already_exists
        - collection.not_active
        - collection_backfill.blocked
        - organization.not_found
        - chip.already_paired
        - chip.key_unusable
        - chip.mac_required
        - chip.bulk_mixed_collections
        - chip.bulk_pending
        - unit.issuance_pending
        - claim.owner_unknown
        - claim.already_to_recipient
        - claim.in_progress
        - claim.locked
        - claim.unit_vaulted
        - claim.unit_held
        - claim.tap_required
        - claim.not_found
        - chip_replacement.locked
        - chip_replacement.in_progress
        - chip_replacement.not_found
        - transfer.owner_unknown
        - transfer.already_to_recipient
        - transfer.in_progress
        - transfer.not_found
        - submission.not_executable
        - vault.not_found
        - vault.already_exists
        - vault.provisioning_failed
        - vault.not_permitted
        - vault.unavailable
        - vault.challenge_invalid
        - vault.address_in_use
        - vault.key_conflict
        - vault.signer_unauthorized
        - vault_connection.not_found
        - vault_connection.not_active
        - vault_connection.already_exists
        - vault_connection.conflict
        - vault.in_progress
        - vault.owner_changed
        - vault.request_not_found
        - vault.request_not_pending
        - vault.consent_invalid
        - vault.consent_used
        - vault.consent_outdated
        - vault.request_rejected
        - vault.request_expired
        - vault.reference_taken
        - tap_intent.not_found
        - tap_intent.disabled
        - tap_intent.too_many_open
        - tap_intent.unit_vaulted
        - tap_intent.redirect_origin_not_allowed
        - tap_intent.expired
        - tap_intent.canceled
        - tap_intent.already_approved
        - tap_intent.already_consumed
        - tap_intent.not_approved
        - tap_intent.action_mismatch
        - tap_intent.tap_not_matched
        - tap_intent.code_required
        - tap_intent.code_incorrect
        - tap_intent.code_attempts_exhausted
        - tap_intent.code_unavailable
        - tap_intent.busy
        - idempotency.key_conflict
        - idempotency.in_progress
        - internal.error
        - webhook_endpoint.not_found
        - webhook_endpoint.deleted
        - webhook_endpoint.limit_reached
        - webhook_endpoint.url_refused
        - webhook_endpoint.signing_unavailable
        - webhook_event_type.unknown
        - webhook_event.not_found
        - webhook_endpoint.disabled
        - webhook_delivery.in_progress
        - webhook_endpoint.not_subscribed
      description: >-
        Stable, namespaced error code in `<resource>.<reason>` form. Branch on
        this rather than on `message` or the HTTP status. This is the catalogue
        as of this spec revision, not a closed set - see
        `ErrorResponse.error.code`.
      example: chip.not_found
    ValidationErrorDetails:
      type: object
      properties:
        formErrors:
          type: array
          items:
            type: string
          description: Issues that apply to the request as a whole rather than one field.
          example: []
        fieldErrors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
          description: Validation issues keyed by the field that failed.
          example:
            name:
              - 'Invalid input: expected string, received undefined'
      required:
        - formErrors
        - fieldErrors
      description: Per-field validation detail. Present only on `validation.failed`.
  securitySchemes:
    ApiKeyBearer:
      type: http
      scheme: bearer
      bearerFormat: end_sk
      description: >-
        Use `Authorization: Bearer end_sk_*` for partner API keys (e.g.
        `end_sk_AbCd_example_api_key`).

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.