> ## 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.

# Open a tap intent

> Opens a tap intent for one unit, so a person can confirm a step in your app by tapping the item with any phone. The response carries `tap_code` to show the person. They tap the item, the page the tap opens lists the unit's open tap intents, and they confirm this one there with the code. The tap intent is then `approved` and Endstate sends `tap_intent.approved`. Consume it within `use_within` seconds of the approval, then perform your own action. A unit can have up to 10 open tap intents. Tap intents must be enabled for the environment.



## OpenAPI

````yaml /openapi.json post /v1/tap-intents
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:
    post:
      tags:
        - Tap intents
      summary: Open a tap intent
      description: >-
        Opens a tap intent for one unit, so a person can confirm a step in your
        app by tapping the item with any phone. The response carries `tap_code`
        to show the person. They tap the item, the page the tap opens lists the
        unit's open tap intents, and they confirm this one there with the code.
        The tap intent is then `approved` and Endstate sends
        `tap_intent.approved`. Consume it within `use_within` seconds of the
        approval, then perform your own action. A unit can have up to 10 open
        tap intents. Tap intents must be enabled for the environment.
      operationId: createTapIntent
      parameters:
        - 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:
              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
            schema:
              $ref: '#/components/schemas/CreateTapIntentRequest'
      responses:
        '201':
          description: The open tap intent and its code.
          content:
            application/json:
              example:
                id: tapi_3f9c1b7a2d4e4f60a1b2c3d4e5f60718
                status: pending
                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: null
                created_at: '2026-11-10T17:02:11.000Z'
                expires_at: '2026-11-10T17:07:11.000Z'
                approved_at: null
                use_by: null
                consumed_at: null
                canceled_at: null
                tap_code: '482913'
              schema:
                $ref: '#/components/schemas/CreatedTapIntent'
        '400':
          description: >-
            The request was malformed or failed validation.


            | Error code | When |

            | --- | --- |

            | `tap_intent.redirect_origin_not_allowed` | `redirect_url` must be
            an http or https URL, https in a live environment, on one of the
            environment's allowed origins. |

            | `validation.failed` | The request failed schema validation. See
            `error.details` for per-field issues. |
          x-error-codes:
            - tap_intent.redirect_origin_not_allowed
            - validation.failed
          content:
            application/json:
              examples:
                tap_intent.redirect_origin_not_allowed:
                  summary: >-
                    `redirect_url` must be an http or https URL, https in a live
                    environment, on one of the environment's allowed origins.
                  value:
                    error:
                      code: tap_intent.redirect_origin_not_allowed
                      message: >-
                        `redirect_url` must be an http or https URL, https in a
                        live environment, on one of the environment's allowed
                        origins.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: >-
                        https://docs.endstate.io/errors/tap-intent-redirect-origin-not-allowed
                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. |

            | `tap_intent.disabled` | Tap intents are not enabled for this
            environment. |
          x-error-codes:
            - auth.forbidden
            - tap_intent.disabled
          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
                tap_intent.disabled:
                  summary: Tap intents are not enabled for this environment.
                  value:
                    error:
                      code: tap_intent.disabled
                      message: Tap intents are not enabled for this environment.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/tap-intent-disabled
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: >-
            The resource does not exist within your organization.


            | Error code | When |

            | --- | --- |

            | `not_found.resource` | The requested resource does not exist
            within your organization. |

            | `unit.not_found` | The unit ID does not exist in your
            organization. |
          x-error-codes:
            - not_found.resource
            - unit.not_found
          content:
            application/json:
              examples:
                not_found.resource:
                  summary: >-
                    The requested resource does not exist within your
                    organization.
                  value:
                    error:
                      code: not_found.resource
                      message: >-
                        The requested resource does not exist within your
                        organization.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/not-found-resource
                unit.not_found:
                  summary: The unit ID does not exist in your organization.
                  value:
                    error:
                      code: unit.not_found
                      message: The unit ID does not exist in your organization.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/unit-not-found
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: >-
            The request conflicts with the resource's current state.


            | Error code | When |

            | --- | --- |

            | `environment.invalid_configuration` | The environment
            configuration is invalid. Contact support before retrying. |

            | `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.too_many_open` | The unit already has 10 open tap
            intents. `details.tap_intent_ids` names them. Cancel one before
            opening another. |

            | `tap_intent.unit_vaulted` | The unit is in a vault, so it has no
            tap to offer. |
          x-error-codes:
            - environment.invalid_configuration
            - idempotency.in_progress
            - idempotency.key_conflict
            - tap_intent.too_many_open
            - tap_intent.unit_vaulted
          content:
            application/json:
              examples:
                environment.invalid_configuration:
                  summary: >-
                    The environment configuration is invalid. Contact support
                    before retrying.
                  value:
                    error:
                      code: environment.invalid_configuration
                      message: >-
                        The environment configuration is invalid. Contact
                        support before retrying.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: >-
                        https://docs.endstate.io/errors/environment-invalid-configuration
                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.too_many_open:
                  summary: >-
                    The unit already has 10 open tap intents.
                    `details.tap_intent_ids` names them. Cancel one before
                    opening another.
                  value:
                    error:
                      code: tap_intent.too_many_open
                      message: >-
                        The unit already has 10 open tap intents.
                        `details.tap_intent_ids` names them. Cancel one before
                        opening another.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/tap-intent-too-many-open
                tap_intent.unit_vaulted:
                  summary: The unit is in a vault, so it has no tap to offer.
                  value:
                    error:
                      code: tap_intent.unit_vaulted
                      message: The unit is in a vault, so it has no tap to offer.
                      request_id: req_8e1a7f50-90ab-4cde-f012-3456789abcde
                      doc_url: https://docs.endstate.io/errors/tap-intent-unit-vaulted
              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:
    CreateTapIntentRequest:
      type: object
      properties:
        rule:
          type: string
          enum:
            - own
          description: >-
            Who may open the intent. `own` opens it on a unit in your own
            environment.
        unit_id:
          type: string
          format: uuid
          description: The unit the person will tap.
        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: >-
            Opaque to Endstate. Stored with surrounding whitespace trimmed,
            returned on reads and in webhooks, and compared exactly when you
            consume.
        ttl:
          type: integer
          minimum: 60
          maximum: 900
          default: 300
          description: Seconds the intent waits for a tap. A tap after that does not count.
        use_within:
          type: integer
          minimum: 60
          maximum: 900
          default: 300
          description: >-
            Seconds after the person approves in which you can consume the
            approval. Fixed here: `use_by` is `approved_at` plus this, and no
            call extends it.
        code:
          type: boolean
          default: true
          description: >-
            Whether the person must enter a code to confirm. On by default: the
            response carries `tap_code` for you to show them. Pass `false` only
            where the person asking is the person holding the item and a wrong
            approval costs little, because anyone who taps the item can then
            confirm.
        redirect_url:
          type: string
          maxLength: 2048
          description: >-
            Where the person is sent after they confirm. `tap_intent=<id>` is
            added to it. Must be on one of the environment's allowed origins.
          example: https://app.example.com/listings/8271/confirm
        display:
          type: object
          properties:
            title:
              type: string
              minLength: 1
              maxLength: 120
              description: >-
                Shown to whoever taps the item, beside the confirm control. Keep
                personal data out of it.
              example: Confirm your listing
          required:
            - title
      required:
        - rule
        - unit_id
        - action
      additionalProperties: false
    CreatedTapIntent:
      allOf:
        - $ref: '#/components/schemas/TapIntent'
        - type: object
          properties:
            tap_code:
              type:
                - string
                - 'null'
              description: >-
                The six digit code to show the person, or null when `code` is
                false. Returned by this response only: no read or webhook
                carries it, and a replay of the same `Idempotency-Key` returns
                null. If you lose it, cancel and open a new tap intent.
              example: '482913'
          required:
            - tap_code
    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.
    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
    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.