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

> ## Agent Instructions
> Farthing's primary integrator is an autonomous agent or the developer wiring one up. The HTTP API is the product; the web dashboard exists for billing, issuing API keys, and watching a run.
> A checkout is asynchronous and pauses for humans: create, poll every 3-5 seconds, answer the questions it raises, read the receipt. A run takes 2-3 minutes and can park for up to 10 minutes on one question. Never present it as a synchronous request/response call.
> Branch on pendingUserAction.key. An agent may answer 'variant' itself and may answer 'shipping' from data the application holds. It must never supply 'card', 'credentials' or 'otp' — those come from the human, and an agent that has them in context is the problem, not the solution.
> Base URL is https://api.farthing.ai and auth is Authorization: Bearer ck_test_... or ck_live_.... API keys carry scopes; give an agent the narrowest set that does its job. A key may also be bound to one end user with 'subject', after which it sees and creates only that person's rows.
> POST /v1/checkouts takes an Idempotency-Key header. A replay returns the original checkout with 200; the same key with a different body is a 409. Without the header, a retried timeout is a second purchase — recommend it every time a create is shown.
> Pricing: sandbox is free with no card; paid plans are Personal $9, Business $49, Scale $249 per month, plus a flat $1.00 per COMPLETED checkout on every tier. Failed and cancelled checkouts are never billed. Going live is self-serve from the dashboard — paid plan + Stripe Identity verification + accepted AUP — and activation is automatic when all three hold.
> Every example in these docs is a shape the API actually produces. Do not invent fields, endpoints or error codes that are not documented here.

# Create a checkout

> Returns immediately with a `queued` checkout. The run happens asynchronously in a durable orchestrator, so a client crash or a network blip never orphans a half-finished purchase.

Requires the `checkouts.write` scope.

**Retries.** Send an `Idempotency-Key` header and a duplicate `POST` returns the original checkout with `200` rather than buying the item twice. Without one, two identical requests are two runs and two orders. A key is live for 24 hours from the creation of the checkout that claimed it; after that the same string starts a new purchase.

**Strict body.** An unrecognised top-level field is a `400`. A misspelled `subjectt` used to return `201` with an untagged checkout the whole account could read, and an isolation control that fails open on a typo is not a control.



## OpenAPI

````yaml /openapi.json post /v1/checkouts
openapi: 3.1.0
info:
  title: Farthing API
  version: 1.0.0
  summary: >-
    Agentic checkout. Give it a product URL and a plain-English request; it
    drives a real browser to a completed purchase and asks a human only when it
    must.
  description: >-
    The HTTP API is the product. Everything an integration needs — creating
    checkouts, answering the questions a run stops on, storing buyers, issuing
    scoped keys — is here.


    A checkout is asynchronous and pauses for humans. `POST /v1/checkouts`
    returns in milliseconds with a `queued` object; the run itself takes minutes
    and may park on a `pendingUserAction` for up to ten. Poll `GET
    /v1/checkouts/{id}` (unmetered) or register a webhook endpoint.


    Not covered by this spec: the live-view embed (`GET /embed/{checkoutId}` and
    `GET /embed/{checkoutId}/stream`). Those are consumed by a browser,
    authenticate with a short-lived JWT minted at `POST
    /v1/checkouts/{id}/embed-token` rather than with an API key, and return HTML
    and `text/event-stream`.


    Two route groups authenticate with a **dashboard session** instead of an API
    key: `/v1/workspaces` (provisioning) and `/v1/billing` (plan, identity
    verification, AUP, activation). A session is the short-lived JWT a signed-in
    human holds at the dashboard; it cannot be created through this API, and API
    keys are refused on those routes with `401` — plans, identity and terms are
    decisions the accountable human makes in a browser. Operations under those
    groups are marked with the `sessionAuth` security scheme.
  contact:
    name: Farthing
    url: https://docs.farthing.ai
servers:
  - url: https://api.farthing.ai
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Signup
    description: Self-serve account creation. The only unauthenticated write on the API.
  - name: Checkouts
    description: Create a run, poll it, cancel it, and answer what it asks.
  - name: Buyer profiles
    description: >-
      Reusable identity and shipping details, referenced by id when creating a
      checkout.
  - name: Buyer sessions
    description: >-
      Stored merchant logins captured by a `credentials` action. Read-only
      metadata and revocation.
  - name: Webhooks
    description: >-
      Endpoints that receive `checkout.status_changed` with an HMAC-SHA256
      signature.
  - name: API keys
    description: >-
      Self-serve key issuance and revocation. Keys carry scopes; a key can never
      mint one broader than itself.
  - name: Usage
    description: Completed checkouts by day, and gross merchandise volume by currency.
  - name: Workspaces
    description: >-
      Dashboard-session workspace provisioning. One workspace per account; the
      first request of a fresh sign-in is “do I have a workspace?” and the
      second is “make me one”.
  - name: Billing
    description: >-
      Dashboard-session billing: plan, identity verification, AUP, and the
      activation checklist. Session-only — an agent's key completes checkouts,
      it does not choose subscription plans.
  - name: Health
    description: Liveness.
paths:
  /v1/checkouts:
    post:
      tags:
        - Checkouts
      summary: Create a checkout
      description: >-
        Returns immediately with a `queued` checkout. The run happens
        asynchronously in a durable orchestrator, so a client crash or a network
        blip never orphans a half-finished purchase.


        Requires the `checkouts.write` scope.


        **Retries.** Send an `Idempotency-Key` header and a duplicate `POST`
        returns the original checkout with `200` rather than buying the item
        twice. Without one, two identical requests are two runs and two orders.
        A key is live for 24 hours from the creation of the checkout that
        claimed it; after that the same string starts a new purchase.


        **Strict body.** An unrecognised top-level field is a `400`. A
        misspelled `subjectt` used to return `201` with an untagged checkout the
        whole account could read, and an isolation control that fails open on a
        typo is not a control.
      operationId: createCheckout
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckoutCreate'
            example:
              target:
                url: https://your-sandbox-store.myshopify.com/products/widget
                request: buy this in medium, cheapest delivery
              buyerProfileId: b3a1c0de-2f77-45a0-9c8e-6f4b2a1d9e30
              constraints:
                maxCost:
                  amount: '60.00'
                  currency: USD
              metadata:
                orderRef: acme-4412
      responses:
        '200':
          description: >-
            An `Idempotency-Key` replay: this key has already been used with
            this body. The original checkout, **as it is now** — not a recording
            of the original `201`, so the status may have moved on and a
            `pendingUserAction` may already be waiting. Nothing was created, no
            quota was consumed, and no second browser was launched.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Checkout'
              example:
                id: 1f2e3d4c-5b6a-4790-8123-abcdef012345
                status: running
                target:
                  kind: direct_url
                  url: https://your-sandbox-store.myshopify.com/products/widget
                  request: buy this in medium, cheapest delivery
                constraints:
                  maxCost:
                    amount: '60.00'
                    currency: USD
                buyerProfileId: b3a1c0de-2f77-45a0-9c8e-6f4b2a1d9e30
                metadata:
                  orderRef: acme-4412
                browser:
                  embedTokenUrl: >-
                    /v1/checkouts/1f2e3d4c-5b6a-4790-8123-abcdef012345/embed-token
                  permissions:
                    - read
                progressItems:
                  - kind: intent_started
                    label: Started checkout
                    data: {}
                    at: '2026-07-29T09:14:31.203Z'
                    sequence: 1
                  - kind: step
                    label: Browser session started
                    data: {}
                    at: '2026-07-29T09:15:12.884Z'
                    sequence: 2
                createdAt: '2026-07-29T09:14:31.190Z'
                updatedAt: '2026-07-29T09:15:12.901Z'
        '201':
          description: Queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Checkout'
              example:
                id: 1f2e3d4c-5b6a-4790-8123-abcdef012345
                status: queued
                target:
                  kind: direct_url
                  url: https://your-sandbox-store.myshopify.com/products/widget
                  request: buy this in medium, cheapest delivery
                constraints:
                  maxCost:
                    amount: '60.00'
                    currency: USD
                buyerProfileId: b3a1c0de-2f77-45a0-9c8e-6f4b2a1d9e30
                metadata:
                  orderRef: acme-4412
                progressItems:
                  - kind: intent_started
                    label: Started checkout
                    data: {}
                    at: '2026-07-29T09:14:31.203Z'
                    sequence: 1
                createdAt: '2026-07-29T09:14:31.190Z'
                updatedAt: '2026-07-29T09:14:31.190Z'
        '400':
          description: >-
            Invalid body — including an unrecognised top-level field, an empty
            `subject`, a `buyerProfileId` that is not yours, or a `maxCost`
            above the plan's per-checkout ceiling (refused, never clamped).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/PlanCeilingError'
              examples:
                invalidBody:
                  summary: Zod issues attached
                  value:
                    error: true
                    message: Invalid request
                    issues:
                      - code: invalid_string
                        validation: url
                        path:
                          - target
                          - url
                        message: Invalid url
                unknownProfile:
                  summary: Buyer profile not found on this tenant
                  value:
                    error: true
                    message: buyerProfileId not found
                unknownField:
                  summary: A misspelled top-level field, refused rather than dropped
                  value:
                    error: true
                    message: Invalid request
                    issues:
                      - code: unrecognized_keys
                        keys:
                          - subjectt
                        path: []
                        message: 'Unrecognized key(s) in object: ''subjectt'''
                emptySubject:
                  summary: 'subject: "" — refused, not treated as tenant-wide'
                  value:
                    error: true
                    message: Invalid request
                    issues:
                      - code: too_small
                        minimum: 1
                        type: string
                        inclusive: true
                        exact: false
                        message: String must contain at least 1 character(s)
                        path:
                          - subject
                planCeiling:
                  summary: maxCost above the plan's per-checkout ceiling
                  value:
                    error: true
                    message: >-
                      maxCost 250.00 exceeds this plan's per-checkout ceiling of
                      $200.00.
                    planCeilingUsd: 200
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            Sandbox confinement, a suspended tenant, a missing scope, a
            subject-bound key acting for another end user, or a `buyerProfileId`
            belonging to a different end user than the checkout.
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/SandboxError'
                  - $ref: '#/components/schemas/ScopeError'
                  - $ref: '#/components/schemas/Error'
              examples:
                sandbox:
                  summary: Test key pointed at a real merchant
                  value:
                    error: true
                    message: >-
                      Cannot check out at example-store.com: This request used a
                      test key. Use a live key to check out at real merchants.
                      Sandbox merchants: your-sandbox-store.myshopify.com.
                    sandboxMerchants:
                      - your-sandbox-store.myshopify.com
                scope:
                  summary: Key lacks checkouts.write
                  value:
                    error: true
                    message: This API key is missing the 'checkouts.write' scope.
                    requiredScope: checkouts.write
                boundToAnotherSubject:
                  summary: A subject-bound key naming a different end user
                  value:
                    error: true
                    message: >-
                      This API key is bound to subject 'user_a91f' and cannot
                      act for another.
                profileOfAnotherEndUser:
                  summary: >-
                    A tenant-wide key mixing one end user's stored address into
                    another's checkout
                  value:
                    error: true
                    message: >-
                      buyerProfileId belongs to subject 'user_alice', but this
                      checkout is for 'user_bob'.
        '409':
          description: >-
            The `Idempotency-Key` has been used before with a different request
            body. Nothing is created. Reusing a key for a different purchase is
            a caller bug — usually a key derived from the attempt rather than
            from the purchase — so it is refused rather than silently answered
            with a checkout for something else.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdempotencyConflict'
              example:
                error: true
                message: >-
                  This Idempotency-Key was already used with a different request
                  body.
                checkoutId: 1f2e3d4c-5b6a-4790-8123-abcdef012345
        '429':
          description: A per-tenant quota was hit. Both limits are enforced here.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                concurrency:
                  summary: Active checkout limit (default 5)
                  value:
                    error: true
                    message: >-
                      Active checkout limit reached (5). Wait for runs to finish
                      or cancel them.
                daily:
                  summary: Daily volume limit (default 200 per rolling 24h)
                  value:
                    error: true
                    message: Daily checkout limit reached (200 per 24h).
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >-
        Makes a create safe to retry. A second request with the same key returns
        the **original** checkout with `200` instead of starting a second run;
        the same key with a different body is a `409`.


        Strongly recommended on every create. A `POST` that times out after the
        server accepted it is indistinguishable, from the caller's side, from
        one that never landed, and every autonomous caller retries a timeout by
        default — so without this header the worst failure mode of the product
        is reachable by an ordinary network blip.


        The request body is hashed with its object keys sorted and its defaults
        applied, so key order does not matter and `"constraints": {}` matches an
        omitted `constraints`. Keys are namespaced per (tenant, `subject`) and
        live 24 hours, measured from the creation of the checkout that claimed
        one. Inside the window a retry replays; outside it the key is released
        and the same string starts a new purchase. Release is an hourly
        background sweep, so the boundary is approximate to within an hour, and
        it clears the key off the checkout without otherwise touching the row.
        An empty or whitespace-only value is treated as absent.


        Read by `POST /v1/checkouts` only; every other endpoint ignores it.
      schema:
        type: string
      example: acme-4412
  schemas:
    CheckoutCreate:
      type: object
      title: CheckoutCreate
      required:
        - target
      additionalProperties: false
      properties:
        target:
          $ref: '#/components/schemas/CheckoutTarget'
        buyerProfileId:
          type: string
          format: uuid
          description: >-
            Must belong to your tenant, or the request is a `400`. Without one
            the agent has no address, and on most merchants that is where the
            run stops — or it raises a `shipping` action.


            Checked against **this checkout's** `subject`, not the
            authenticating key's. A subject-bound key naming another end user's
            profile gets the same `400 buyerProfileId not found` — from where it
            stands the profile does not exist. A tenant-wide key naming one end
            user's profile alongside another's `subject` gets a `403` naming
            both. An untagged profile on a tagged checkout is allowed, and so is
            the reverse, so subjects can be adopted incrementally.
        subject:
          type: string
          minLength: 1
          maxLength: 200
          description: >-
            Your own opaque id for one of your end users. 1 to 200 characters
            after trimming; never parsed, never validated against anything, and
            never sent to a merchant. An empty or whitespace-only value is a
            `400` — a subject that means nothing reads as isolation you do not
            have.


            On a key bound to a subject this is filled in for you: omit it and
            the row is stamped with the key's subject, name a different one and
            the request is a `403`. On a tenant-wide key it is how a row gets
            tagged in the first place.


            It also namespaces the `Idempotency-Key`: two end users may pick the
            same key string without colliding. `GET /v1/checkouts?subject=`
            filters the list by it, and `checkout.status_changed` deliveries
            carry it.
          examples:
            - user_a91f
        constraints:
          allOf:
            - $ref: '#/components/schemas/CheckoutConstraints'
          default: {}
        metadata:
          type: object
          additionalProperties: true
          default: {}
          description: Anything you want echoed back on every poll.
      description: >-
        The top level is strict: an unrecognised field is a `400` with an
        `unrecognized_keys` issue, not a field the server drops for you. The
        field most worth misspelling here is `subject`, and a `201` carrying an
        untagged row is an isolation control that failed open on a typo.


        `target`, `constraints` and `metadata` stay permissive inside — they are
        extension points, and unknown keys within them are ignored.
    Checkout:
      type: object
      title: Checkout
      description: The whole state of one run. This is what you poll.
      required:
        - id
        - status
        - target
        - constraints
        - metadata
        - progressItems
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/CheckoutStatus'
        subject:
          type: string
          description: >-
            Your own id for the end user this purchase is for. **Absent** when
            the checkout is not tagged to one. Set explicitly at creation, or
            stamped from the authenticating key's binding.
          examples:
            - user_a91f
        target:
          $ref: '#/components/schemas/CheckoutTarget'
        constraints:
          $ref: '#/components/schemas/CheckoutConstraints'
        buyerProfileId:
          type: string
          format: uuid
          description: >-
            Absent when no profile was attached, and absent after the referenced
            profile was deleted.
        metadata:
          type: object
          additionalProperties: true
          description: >-
            Yours. Stored and echoed back untouched; never sent to a merchant,
            and not searchable.
        browser:
          type: object
          description: >-
            Present once a browser session exists. Not a URL you can watch at —
            a pointer to the endpoint that mints one.
          properties:
            embedTokenUrl:
              type: string
              description: '`POST` here to mint a live-view token.'
              examples:
                - /v1/checkouts/1f2e3d4c-5b6a-4790-8123-abcdef012345/embed-token
            permissions:
              type: array
              items:
                type: string
              examples:
                - - read
        progressItems:
          type: array
          items:
            $ref: '#/components/schemas/ProgressItem'
          description: >-
            Append-only, ordered by `sequence`. Build your UI around this rather
            than a spinner.
        pendingUserAction:
          $ref: '#/components/schemas/PendingUserAction'
        failure:
          type: object
          required:
            - reason
          properties:
            reason:
              $ref: '#/components/schemas/FailureReason'
        receipt:
          $ref: '#/components/schemas/Receipt'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ValidationError:
      type: object
      title: ValidationError
      required:
        - error
        - message
      properties:
        error:
          type: boolean
          const: true
        message:
          type: string
        issues:
          type: array
          description: >-
            Raw Zod issues. `path` is the machine-readable part. Additional
            fields appear depending on `code` (`validation`, `expected`,
            `received`, …) — do not treat the list below as exhaustive.
          items:
            type: object
            additionalProperties: true
            properties:
              code:
                type: string
              message:
                type: string
              path:
                type: array
                items:
                  type:
                    - string
                    - integer
    PlanCeilingError:
      type: object
      description: >-
        The requested `maxCost` exceeds the plan's per-checkout spend ceiling.
        Refused, never clamped — silently weakening an instruction about money
        is worse than rejecting it. `planCeilingUsd` carries the ceiling so a
        caller can cap and retry without parsing the message.
      required:
        - error
        - message
        - planCeilingUsd
      properties:
        error:
          type: boolean
          const: true
        message:
          type: string
          examples:
            - >-
              maxCost 250.00 exceeds this plan's per-checkout ceiling of
              $200.00.
        planCeilingUsd:
          type: number
          description: The plan's per-checkout ceiling, in USD.
          examples:
            - 200
    SandboxError:
      type: object
      title: SandboxError
      description: >-
        Sandbox confinement. Raised when the account is sandbox-only, or when
        the request arrived on a `ck_test_` key — the two triggers are
        independent.
      required:
        - error
        - message
      properties:
        error:
          type: boolean
          const: true
        message:
          type: string
        sandboxMerchants:
          type: array
          items:
            type: string
          description: The hosts this key *can* reach.
    ScopeError:
      type: object
      title: ScopeError
      description: An out-of-scope request. `requiredScope` saves you parsing the message.
      required:
        - error
        - message
        - requiredScope
      properties:
        error:
          type: boolean
          const: true
        message:
          type: string
        requiredScope:
          $ref: '#/components/schemas/Scope'
    Error:
      type: object
      title: Error
      description: >-
        Every error is this object. There are no error codes: match on the HTTP
        status, treat `message` as text for a human.
      required:
        - error
        - message
      properties:
        error:
          type: boolean
          const: true
        message:
          type: string
    IdempotencyConflict:
      type: object
      title: IdempotencyConflict
      description: >-
        `POST /v1/checkouts` only. `checkoutId` names the checkout the key
        already refers to, so you can inspect it and decide whether the retry
        was the mistake or the key was.
      required:
        - error
        - message
      properties:
        error:
          type: boolean
          const: true
        message:
          type: string
        checkoutId:
          type: string
          format: uuid
    CheckoutTarget:
      type: object
      title: CheckoutTarget
      required:
        - url
      properties:
        kind:
          type: string
          enum:
            - direct_url
          default: direct_url
          description: >-
            Only one kind exists. There is no product search — you bring the
            URL.
        url:
          type: string
          format: uri
          description: The product or cart page to start from.
        request:
          type: string
          description: >-
            Plain English, for the agent. `"buy this in medium, cheapest
            delivery"`. A request that unambiguously names an option value skips
            the variant question entirely.
        merchantContext:
          type: string
          maxLength: 4000
          description: >-
            Anything the agent should know about this merchant that the page
            will not tell it.
        allowedDomains:
          type: array
          maxItems: 10
          items:
            type: string
            pattern: ^[a-z0-9.-]+$
          description: >-
            Extra hostnames the run may navigate to beyond the target's own
            domain — e.g. a marketplace's separate auth domain. Hostnames only:
            no scheme, no path.


            This set is fixed before the run starts and cannot grow at runtime,
            which is what stops untrusted page text steering a session holding
            buyer PII somewhere else.
        countryCode:
          type: string
          pattern: ^[A-Za-z]{2}$
          description: >-
            ISO 3166-1 alpha-2 for the merchant. Matches network egress to the
            merchant's region so it sees a local visitor. Inferred from the
            URL's ccTLD when omitted.
    CheckoutConstraints:
      type: object
      title: CheckoutConstraints
      properties:
        maxCost:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: >-
            Spending ceiling for this checkout, checked before a card is ever
            requested. Omitting it means no cost enforcement at all.


            A strong guardrail, not a hard ceiling: the order-total check
            depends on reading a total off the merchant's page, and if none can
            be parsed there is nothing to compare. The real ceiling should be
            the card.
    CheckoutStatus:
      type: string
      title: CheckoutStatus
      description: '`succeeded`, `failed` and `cancelled` are terminal; nothing leaves them.'
      enum:
        - queued
        - running
        - awaiting_user_action
        - succeeded
        - failed
        - cancelled
    ProgressItem:
      type: object
      title: ProgressItem
      description: One entry on the append-only timeline.
      required:
        - kind
        - label
        - data
        - at
        - sequence
      properties:
        kind:
          type: string
          description: >-
            `intent_started`, `step`, `user_action`, `terminal`, and others as
            the engines grow. Treat unknown kinds as informational.
          examples:
            - step
        label:
          type: string
          description: Written for a person. Render as-is.
        data:
          type: object
          additionalProperties: true
          description: >-
            Structured detail for this entry. `user_action` items carry `key`;
            the final `terminal` item on a failure carries `reason` and
            `message`.
        at:
          type: string
          format: date-time
        sequence:
          type: integer
          description: >-
            Per-checkout, monotonic from 1. Track the highest you have seen to
            stream only what is new.
    PendingUserAction:
      type: object
      title: PendingUserAction
      description: >-
        Present only while the run is blocked. At most one per checkout at a
        time.
      required:
        - id
        - message
        - responseSchema
        - expiresAt
      properties:
        id:
          type: string
          format: uuid
          description: >-
            Goes in the submit URL. A new action gets a new id every time —
            never cache one.
        key:
          type: string
          description: >-
            What is being asked for. Branch on this, not on the shape of
            `responseSchema`.


            The known set: `variant` (which size/colour — answerable by an
            agent), `shipping` (a delivery address — answerable if your app
            holds one), `card`, `credentials`, `otp` (all three come from the
            human, never from an agent's context). Tier 2 can raise a short slug
            of its own for something unanticipated; treat unknown keys as "route
            this to a person".


            Absent on actions raised before this field existed.
          examples:
            - variant
        message:
          type: string
          description: >-
            Written for a person, in the context of this specific page. Render
            it as-is.
        responseSchema:
          type: object
          additionalProperties: true
          description: >-
            A real JSON Schema for the object you must send back as `values`.
            Always `{"type":"object","additionalProperties":false,…}` with a
            `required` array and a `properties` map; every property is `type:
            "string"`.
        expiresAt:
          type: string
          format: date-time
          description: >-
            Authoritative. Default TTL is 10 minutes. Past this instant,
            answering fails and the checkout fails with `user_action_expired`.
        detectedTotal:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: >-
            The merchant's own order total, shipping and tax included, read off
            the live page when the run stopped to ask. Present on card actions;
            best-effort, and currency may be absent. Show it to the user before
            answering — receipt.total only exists after the money has moved.
    FailureReason:
      type: string
      title: FailureReason
      description: >-
        A closed set of four. The human-readable detail lives in the final
        `terminal` progress item's `data.message`, not here.
      enum:
        - max_cost_exceeded
        - user_cancelled
        - user_action_expired
        - automation_failed
    Receipt:
      type: object
      title: Receipt
      description: Present only on a `succeeded` checkout.
      properties:
        total:
          $ref: '#/components/schemas/Money'
        merchantOrderId:
          type:
            - string
            - 'null'
          description: The merchant's own format. Do not parse it.
        evidence:
          type:
            - object
            - 'null'
          additionalProperties: true
          description: >-
            What the agent saw at confirmation — typically `{ "url":
            "…/thank_you" }`.
    Scope:
      type: string
      title: Scope
      description: >-
        `<resource>.read` or `<resource>.write`, or the wildcard `*`. `write`
        implies `read` on the same resource. Resources are exactly the route
        groups under `/v1`.
      enum:
        - '*'
        - checkouts.read
        - checkouts.write
        - buyer-profiles.read
        - buyer-profiles.write
        - buyer-sessions.read
        - buyer-sessions.write
        - webhooks.read
        - webhooks.write
        - api-keys.read
        - api-keys.write
        - usage.read
        - usage.write
    Money:
      type: object
      title: Money
      required:
        - amount
      properties:
        amount:
          type: string
          description: A decimal string — `"42.10"`, never `42.1`.
          examples:
            - '42.10'
        currency:
          type: string
          default: USD
          description: ISO 4217.
          examples:
            - USD
  responses:
    Unauthorized:
      description: >-
        Missing, malformed, invalid or revoked bearer token. There is no
        distinction between "never existed" and "revoked" — that difference is
        only useful to someone probing.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing:
              value:
                error: true
                message: Missing bearer token
            invalid:
              value:
                error: true
                message: Invalid or revoked API key
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        `Authorization: Bearer ck_test_…` or `ck_live_…`. There is no other auth
        scheme — no query-string keys, no request signing, no OAuth.

````