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

# Shop and pay with the holder's saved card

> Buys from online merchants (Amazon, Walmart, Target, Best Buy, DoorDash and more) and pays with the card the account holder saved themselves (see `GET /get-saved-cards`). One conversation per purchase:

1. Send what to buy as `ask`, in plain language. Pass `delivery_address` on this first call when you know it.
2. Relay `reply` to the holder and send their answers back as `ask` with the same `conversation_id`, until a `cart` comes back.
3. Show the holder the cart. Once they agree, send its `hash` as `confirm` with the `conversation_id`. Nothing can be charged before this.
4. The confirm answers `awaiting_approval` with an `approval_url`. **Send it to your human; never open it yourself.** They approve on their own device with their passkey. Then send the same confirm again to get `order_placed`.

A turn runs against a live merchant and can take up to two minutes. This call waits about 45 seconds: a turn that finishes in time comes back complete (`state: done`); a slower one comes back as `state: running` with a `turn_id` to poll with `GET /get-purchase-turn`.

**This route is free.** The holder's own card pays the merchant, and no identity verification is needed.



## OpenAPI

````yaml /api-reference/openapi.json post /buy-with-saved-card
openapi: 3.1.0
info:
  title: Laso Finance x402 API
  version: 1.0.0
  x-docs-revision: 0be974c98eea
  x-docs-manifest: https://laso.finance/.well-known/docs-version.json
  contact:
    email: agents+support@laso.finance
  x-guidance: >-
    Laso Finance is a payment-gated (x402) API that lets an AI agent spend USDC
    on real-world financial products: prepaid cards (U.S. and international),
    gift cards, push-to-card transfers to USD/EUR/GBP debit cards, and
    Venmo/PayPal payouts.


    Payment: every paid route is an x402 v2 endpoint. Call it with no payment
    header to receive a 402 challenge listing the accepted networks, then replay
    with a signed USDC payment. Both Base (eip155:8453) and Solana
    (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp) are accepted on every paid route;
    the caller picks either chain.


    Identity: `GET /auth` is free and identity-only. Prove wallet ownership with
    a `SIGN-IN-WITH-X` (CAIP-122) header to receive a Firebase id_token, then
    send that token as a Bearer credential to the authenticated read routes
    (`get-card-data`, `get-account-balance`, `get-kyc-status`, etc.). Paid
    routes also return fresh auth credentials in their response, so a payment is
    never required just to obtain a token.


    Recommended flow: (1) `GET /auth` to establish identity, (2) call a paid
    route (e.g. `GET /get-card`) to purchase a product, paying USDC on Base or
    Solana, (3) poll the authenticated read routes with the returned Bearer
    token to fetch the resulting card/transfer details. Full machine-readable
    instructions live at https://laso.finance/SKILL.md.
  description: >-
    Payment-gated API for Laso Finance. All paywalled routes use the x402
    protocol — the caller includes a USDC payment header on Base (eip155:8453)
    or Solana (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp) and the server verifies
    payment before processing. Free routes require no payment header. The same
    402 also carries an MPP (Machine Payments Protocol) challenge in
    WWW-Authenticate; an MPP client pays with USDC on Base by replaying with
    `Authorization: Payment ...`, and routes, prices, and responses are
    identical.


    ## Getting started


    To set up a wallet for making x402 payments, choose a provider:


    - **Locus** (default): https://paywithlocus.com/SKILL.md

    - **Sponge**: https://wallet.paysponge.com/skill.md — automatic x402 service
    discovery

    - **Ampersend**: https://www.ampersend.ai/getting-started.md — self-custody
    on Base or Solana with dual-approval spending limits. Laso Finance is a
    default skill, so no manual endpoint registration is needed.


    ## How x402 works


    1. Call a paywalled endpoint without a payment header → receive a `402
    Payment Required` response containing payment details (price, recipient
    address, network).

    2. Construct an x402 payment header using the details from the 402 response.

    3. Replay the request with the payment header → the server verifies payment
    and processes the request.


    ## Authentication flow


    `GET /auth` is free: callers prove wallet ownership by sending a
    `SIGN-IN-WITH-X` header (CAIP-122 wallet signature). Paywalled routes
    (`/get-card`, `/order-gift-card`, `/get-push-to-card`, `/order-intl-card`)
    also return fresh auth credentials in their responses, so a payment is never
    required just to obtain a token.


    Most routes return auth credentials (`id_token`, `refresh_token`,
    `expires_in`). Use the `id_token` as a Bearer token to call authenticated
    Laso Finance endpoints like `/get-card-data`. When the `id_token` expires,
    use `POST /auth` with `grant_type: refresh_token` to get a new one.


    ## Important notes


    The `/get-card` USA prepaid card endpoint is U.S. only — issued in USD,
    usable at U.S.-based merchants only, and physical goods must ship to a U.S.
    address. For non-U.S. merchants or non-USD currencies, use `GET
    /order-intl-card` instead (international prepaid card, admin-fulfilled
    within 24 hours). All cards are intended for the caller's own use.


    ## Rate limits


    Every response carries the request budget so you can pace yourself without
    probing for a limit:


    | Header | Meaning |

    | --- | --- |

    | `RateLimit-Limit` | Requests permitted per window |

    | `RateLimit-Remaining` | Requests still available |

    | `RateLimit-Reset` | Seconds until the window rolls over |

    | `RateLimit-Policy` | The policy these numbers describe, as
    `limit;w=seconds` |


    The same values are repeated as `X-RateLimit-*` for clients that only parse
    that spelling.


    Most routes advertise the service-wide ceiling. `POST /signup` enforces its
    own per-IP budget on top of it and overwrites these headers with its own
    numbers. `POST /refresh-card-data` is limited per card rather than per
    caller, so its headers keep the service-wide values and the per-card budget
    is reported only on rejection.


    Every rejection is a `429` carrying `Retry-After` in seconds and a matching
    `retry_after_seconds` field in the body, computed from the limit that
    actually rejected the request. Wait that long and retry once. Do not retry
    in a tight loop.


    For step-by-step instructions, read https://laso.finance/SKILL.md
servers:
  - url: https://laso.finance
    description: Production
security: []
paths:
  /buy-with-saved-card:
    post:
      tags:
        - cards
      summary: Shop and pay with the holder's saved card
      description: >-
        Buys from online merchants (Amazon, Walmart, Target, Best Buy, DoorDash
        and more) and pays with the card the account holder saved themselves
        (see `GET /get-saved-cards`). One conversation per purchase:


        1. Send what to buy as `ask`, in plain language. Pass `delivery_address`
        on this first call when you know it.

        2. Relay `reply` to the holder and send their answers back as `ask` with
        the same `conversation_id`, until a `cart` comes back.

        3. Show the holder the cart. Once they agree, send its `hash` as
        `confirm` with the `conversation_id`. Nothing can be charged before
        this.

        4. The confirm answers `awaiting_approval` with an `approval_url`.
        **Send it to your human; never open it yourself.** They approve on their
        own device with their passkey. Then send the same confirm again to get
        `order_placed`.


        A turn runs against a live merchant and can take up to two minutes. This
        call waits about 45 seconds: a turn that finishes in time comes back
        complete (`state: done`); a slower one comes back as `state: running`
        with a `turn_id` to poll with `GET /get-purchase-turn`.


        **This route is free.** The holder's own card pays the merchant, and no
        identity verification is needed.
      operationId: buyWithSavedCard
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ask:
                  type: string
                  maxLength: 2000
                  description: >-
                    What the holder wants, in plain language. Required unless
                    confirming.
                conversation_id:
                  type: string
                  description: The conversation to continue. Required with `confirm`.
                confirm:
                  type: string
                  pattern: ^[0-9a-f]{16}$
                  description: >-
                    A cart's `hash` from an earlier turn, to place exactly that
                    cart.
                delivery_address:
                  type: object
                  description: >-
                    Where to ship. Send it on the call that asks for the cart (a
                    cart is bound to the address it was shown for) and on the
                    confirm.
                  required:
                    - street
                    - city
                    - state
                    - zip
                  properties:
                    street:
                      type: string
                    address2:
                      type: string
                    city:
                      type: string
                    state:
                      type: string
                      description: Two-letter state or province code.
                    zip:
                      type: string
                      description: US ZIP or Canadian postal code.
                    phone:
                      type: string
                      description: Recipient phone. Retail shipping requires one.
                    name:
                      type: string
                      description: Recipient name for the label.
                    can_leave_at_door:
                      type: boolean
                    country:
                      type: string
                      description: >-
                        ISO 3166-1 alpha-2. Inferred from the postal code when
                        omitted.
            example:
              ask: a 16 oz bag of Colombian ground coffee from Amazon
              delivery_address:
                street: 1900 Jefferson St
                city: San Francisco
                state: CA
                zip: '94123'
                phone: '+14155550100'
                name: Ada Lovelace
      responses:
        '200':
          description: The turn, complete or still running.
          content:
            application/json:
              schema:
                type: object
                properties:
                  turn_id:
                    type: string
                    description: >-
                      This turn's id. Poll `GET /get-purchase-turn` with it
                      while `state` is `running`.
                  state:
                    type: string
                    enum:
                      - running
                      - done
                      - failed
                    description: >-
                      `running` means the turn is still talking to the merchant;
                      the fields below appear once it is `done`.
                  created_at:
                    type: integer
                    description: Milliseconds since the epoch.
                  conversation_id:
                    type: string
                    description: Send it back on every follow-up and on the confirm.
                  status:
                    type: string
                    enum:
                      - needs_input
                      - awaiting_approval
                      - in_progress
                      - order_placed
                      - declined
                      - conflict
                    description: >-
                      What to do next. `needs_input`: a question or a cart
                      waiting on a confirm. `awaiting_approval`: send
                      `approval_url` to the holder, then repeat the same
                      confirm. `in_progress`: the order is already being placed;
                      read `GET /get-purchase-conversation` instead of
                      confirming again. `order_placed`. `declined`: see
                      `decline_code`. `conflict`: nothing ran, see `error_code`
                      and confirm the fresh cart's hash. Never branch on
                      `reply`.
                  reply:
                    type: string
                    description: >-
                      The shopping assistant's turn as prose, ready to show the
                      holder.
                  cart:
                    oneOf:
                      - type: object
                        properties:
                          merchant:
                            type: string
                            description: Merchant id, e.g. `retail` or `doordash`.
                          merchant_name:
                            type: string
                            description: Display name, e.g. `Amazon`.
                          items:
                            type: array
                            items:
                              type: object
                              properties:
                                name:
                                  type: string
                                quantity:
                                  type: integer
                                price_cents:
                                  type:
                                    - integer
                                    - 'null'
                                product_id:
                                  type:
                                    - string
                                    - 'null'
                                  description: >-
                                    The merchant's id for the line, usually its
                                    product link.
                          fees_cents:
                            type: integer
                            description: All fees combined, including the service fee.
                          tip_cents:
                            type: integer
                          total_cents:
                            type: integer
                            description: The all-in total to show the account holder.
                          approved_ceiling_cents:
                            type:
                              - integer
                              - 'null'
                            description: >-
                              The most the merchant may charge once final tax
                              and shipping settle. This is what the holder's
                              approval covers; any unused amount is released.
                              Null when the merchant prices the cart exactly.
                          hash:
                            type: string
                            description: >-
                              Identity of exactly this cart. Send it back as
                              `confirm` to place it.
                      - type: 'null'
                    description: The most recently shown cart.
                  carts:
                    type: array
                    items:
                      type: object
                      properties:
                        merchant:
                          type: string
                          description: Merchant id, e.g. `retail` or `doordash`.
                        merchant_name:
                          type: string
                          description: Display name, e.g. `Amazon`.
                        items:
                          type: array
                          items:
                            type: object
                            properties:
                              name:
                                type: string
                              quantity:
                                type: integer
                              price_cents:
                                type:
                                  - integer
                                  - 'null'
                              product_id:
                                type:
                                  - string
                                  - 'null'
                                description: >-
                                  The merchant's id for the line, usually its
                                  product link.
                        fees_cents:
                          type: integer
                          description: All fees combined, including the service fee.
                        tip_cents:
                          type: integer
                        total_cents:
                          type: integer
                          description: The all-in total to show the account holder.
                        approved_ceiling_cents:
                          type:
                            - integer
                            - 'null'
                          description: >-
                            The most the merchant may charge once final tax and
                            shipping settle. This is what the holder's approval
                            covers; any unused amount is released. Null when the
                            merchant prices the cart exactly.
                        hash:
                          type: string
                          description: >-
                            Identity of exactly this cart. Send it back as
                            `confirm` to place it.
                    description: Every open cart in the conversation, oldest first.
                  catalog:
                    type: array
                    description: >-
                      The last product search. Empty on a turn that did not
                      search again, so keep the previous one.
                    items:
                      type: object
                      properties:
                        product_id:
                          type: string
                          description: The merchant's product link.
                        name:
                          type:
                            - string
                            - 'null'
                        price_cents:
                          type:
                            - integer
                            - 'null'
                        image_url:
                          type:
                            - string
                            - 'null'
                  unmatched:
                    type: array
                    description: >-
                      Things asked for that did not make it into a cart, with a
                      reason. Show these rather than guessing from `reply`.
                    items:
                      type: object
                      properties:
                        merchant:
                          type: string
                        requested:
                          type: string
                        reason:
                          type: string
                          enum:
                            - not_found
                            - unavailable
                        detail:
                          type:
                            - string
                            - 'null'
                  order_id:
                    type:
                      - string
                      - 'null'
                    description: Set when this turn placed an order.
                  decline_code:
                    type:
                      - string
                      - 'null'
                    description: >-
                      The machine reason behind `declined`, e.g.
                      `items_unavailable`.
                  approval_url:
                    type:
                      - string
                      - 'null'
                    description: >-
                      Send to the holder when `status` is `awaiting_approval`.
                      Never open it yourself.
                  charge_status:
                    type:
                      - string
                      - 'null'
                    enum:
                      - none
                      - confirming
                      - settled
                      - unknown
                      - null
                    description: >-
                      Whether money moved: `none`, `confirming`, `settled`, or
                      `unknown` (do not retry; read the conversation).
                  card_last4:
                    type:
                      - string
                      - 'null'
                    description: The saved card that paid, for naming it to the holder.
                  error_code:
                    type:
                      - string
                      - 'null'
                    description: >-
                      On `conflict`: `cart_changed`, `turn_in_progress`,
                      `delivery_address_changed`, or
                      `delivery_address_bound_after_cart`.
                  error:
                    type: string
                    description: Present when `state` is `failed`.
                  note:
                    type: string
                    description: The next step.
              example:
                turn_id: q8FvT2kLmN3pR5sX7yZa
                state: done
                created_at: 1790366020000
                conversation_id: conv_49ec10bf959bba5559e67bac
                status: needs_input
                reply: >-
                  Added it to your cart from Amazon. Total: $19.46. Let me know
                  if you're ready to place it.
                cart:
                  merchant: retail
                  merchant_name: Amazon
                  items:
                    - name: >-
                        Cafe Quindio Medium Roast 100% Colombian Ground Coffee,
                        16 oz
                      quantity: 1
                      price_cents: 1899
                      product_id: https://www.amazon.com/dp/B0C91LZ8PK
                  fees_cents: 47
                  tip_cents: 0
                  total_cents: 1946
                  approved_ceiling_cents: 3073
                  hash: 5783c0c78f1c16a2
                order_id: null
                decline_code: null
                approval_url: null
                charge_status: null
                card_last4: null
                error_code: null
                note: >-
                  Show the account holder the cart (items, total_cents,
                  approved_ceiling_cents). Once they agree, send its hash as
                  confirm with this conversation_id. To change it, send another
                  ask instead.
        '400':
          description: >-
            A malformed request (the `error` names the field), or no card is
            saved on this account yet. Call `POST /create-saved-card-link` and
            send the returned `url` to the account holder.
        '409':
          description: >-
            A request with this `Idempotency-Key` is still being processed
            (`code: idempotency_key_in_progress`). Wait a moment and retry with
            the same key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: A request with this Idempotency-Key is still being processed
                code: idempotency_key_in_progress
        '422':
          description: >-
            The `Idempotency-Key` was already used for a different request body
            or route (`code: idempotency_key_reused`). Use a new key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Idempotency-Key reused with a different request
                code: idempotency_key_reused
      security:
        - BearerAuth: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 255
      description: >-
        Optional client-generated unique key (a UUID works) that makes this
        write safe to retry. A retry with the same key within 24 hours returns
        the recorded response of the original request, marked with an
        `Idempotency-Replayed: true` header, instead of performing the write
        again. Reusing a key with a different body or route is rejected with 422
        (`code: idempotency_key_reused`); a retry that arrives while the
        original is still running gets 409 (`code:
        idempotency_key_in_progress`), so wait a moment and retry with the same
        key. Keys are scoped to the authenticated account.
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
  securitySchemes:
    BearerAuth:
      type: apiKey
      in: header
      name: Authorization
      description: >-
        Firebase ID token from `/auth` or any paid route, sent as a Bearer
        token: `Authorization: Bearer <id_token>` (the `Bearer ` prefix is
        required).

````

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