---
title: "Shop and pay with the holder's saved card"
method: POST
path: "/buy-with-saved-card"
tags: ["cards"]
---

# Shop and pay with the holder's saved card

`POST /buy-with-saved-card`

## Request

- Base URL: `https://laso.finance`
- URL: `https://laso.finance/buy-with-saved-card`
- Auth: API key in header `Authorization`

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.

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `ask` string: What the holder wants, in plain language. Required unless confirming.
  - `conversation_id` string: The conversation to continue. Required with `confirm`.
  - `confirm` string: A cart's `hash` from an earlier turn, to place exactly that cart.
  - `delivery_address` object: 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.
    - `street` string, required
    - `address2` string
    - `city` string, required
    - `state` string, required: Two-letter state or province code.
    - `zip` string, required: US ZIP or Canadian postal code.
    - `phone` string: Recipient phone. Retail shipping requires one.
    - `name` string: Recipient name for the label.
    - `can_leave_at_door` boolean
    - `country` string: ISO 3166-1 alpha-2. Inferred from the postal code when omitted.

## Response `200`

The turn, complete or still running.

- object
  - `turn_id` string: This turn's id. Poll `GET /get-purchase-turn` with it while `state` is `running`.
  - `state` 'running' | 'done' | 'failed': `running` means the turn is still talking to the merchant; the fields below appear once it is `done`.
  - `created_at` integer: Milliseconds since the epoch.
  - `conversation_id` string: Send it back on every follow-up and on the confirm.
  - `status` 'needs_input' | 'awaiting_approval' | 'in_progress' | 'order_placed' | 'declined' | 'conflict': 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` string: The shopping assistant's turn as prose, ready to show the holder.
  - `cart` object, nullable: The most recently shown cart.
    - `merchant` string: Merchant id, e.g. `retail` or `doordash`.
    - `merchant_name` string: Display name, e.g. `Amazon`.
    - `items` object[]
      - `name` string
      - `quantity` integer
      - `price_cents` integer, nullable
      - `product_id` string, nullable: The merchant's id for the line, usually its product link.
    - `fees_cents` integer: All fees combined, including the service fee.
    - `tip_cents` integer
    - `total_cents` integer: The all-in total to show the account holder.
    - `approved_ceiling_cents` integer, nullable: 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` string: Identity of exactly this cart. Send it back as `confirm` to place it.
  - `carts` object[]: Every open cart in the conversation, oldest first.
    - `merchant` string: Merchant id, e.g. `retail` or `doordash`.
    - `merchant_name` string: Display name, e.g. `Amazon`.
    - `items` object[]
      - `name` string
      - `quantity` integer
      - `price_cents` integer, nullable
      - `product_id` string, nullable: The merchant's id for the line, usually its product link.
    - `fees_cents` integer: All fees combined, including the service fee.
    - `tip_cents` integer
    - `total_cents` integer: The all-in total to show the account holder.
    - `approved_ceiling_cents` integer, nullable: 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` string: Identity of exactly this cart. Send it back as `confirm` to place it.
  - `catalog` object[]: The last product search. Empty on a turn that did not search again, so keep the previous one.
    - `product_id` string: The merchant's product link.
    - `name` string, nullable
    - `price_cents` integer, nullable
    - `image_url` string, nullable
  - `unmatched` object[]: Things asked for that did not make it into a cart, with a reason. Show these rather than guessing from `reply`.
    - `merchant` string
    - `requested` string
    - `reason` 'not_found' | 'unavailable'
    - `detail` string, nullable
  - `order_id` string, nullable: Set when this turn placed an order.
  - `decline_code` string, nullable: The machine reason behind `declined`, e.g. `items_unavailable`.
  - `approval_url` string, nullable: Send to the holder when `status` is `awaiting_approval`. Never open it yourself.
  - `charge_status` 'none' | 'confirming' | 'settled' | 'unknown' | 'null', nullable: Whether money moved: `none`, `confirming`, `settled`, or `unknown` (do not retry; read the conversation).
  - `card_last4` string, nullable: The saved card that paid, for naming it to the holder.
  - `error_code` string, nullable: On `conflict`: `cart_changed`, `turn_in_progress`, `delivery_address_changed`, or `delivery_address_bound_after_cart`.
  - `error` string: Present when `state` is `failed`.
  - `note` string: The next step.

## Other responses

- `400`: 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`: A request with this `Idempotency-Key` is still being processed (`code: idempotency_key_in_progress`). Wait a moment and retry with the same key.
- `422`: The `Idempotency-Key` was already used for a different request body or route (`code: idempotency_key_reused`). Use a new key.

---

[API](https://skmtc.dev/laso/apis/laso-finance-x402-api.md) · [All operations](https://skmtc.dev/laso/apis/laso-finance-x402-api/llms.txt) · [OpenAPI document](https://skmtc.dev/laso/apis/laso-finance-x402-api/revisions/ff8ce20b49ba?raw)
