# Quotes

A quote is a priced, time-bound snapshot. It binds the offers, quantities, currency, discount,
FX snapshot and total for 15 minutes. It is not a payment and it does not reserve stock.

```http
POST /v1/quotes
```

Required scope: `catalog:read` in the current handler. The target contract uses
`quotes:write`; include both on a key that creates quotes during the rollout.

## Request

```json
{
  "currency": "USD",
  "items": [
    { "offerId": "11111111-1111-4111-8111-111111111111", "quantity": 2 }
  ]
}
```

| Field | Rule |
| --- | --- |
| `currency` | `USD`, `EUR` or `GBP`. Defaults to `USD` when omitted. |
| `items` | At least one line. Maximum 200 lines. |
| `offerId` | Opaque id from `GET /v1/catalog`. |
| `quantity` | Integer, at least 1. Total across lines must be 1 to 5,000. |

Agent defaults are 50 lines and 500 units per order unless the principal raises them. Those
caps are part of the target agent contract and are not enforced by the current quote handler.

`Idempotency-Key` is not required on quote creation. If you send one, it is included in the
signature but does not deduplicate quotes. A retry creates another quote with a new id.

## Response

```json
{
  "quoteId": "22222222-2222-4222-8222-222222222222",
  "status": "open",
  "currency": "USD",
  "totalMinor": 1618,
  "expiresAt": "2026-11-04T03:15:00.000Z",
  "fxAsOf": "2026-11-04T02:59:00.000Z",
  "capped": false,
  "lines": [
    {
      "offerId": "11111111-1111-4111-8111-111111111111",
      "quantity": 2,
      "listUnitMinor": 899,
      "finalUnitMinor": 809,
      "discountUnitMinor": 90,
      "requestedPercent": 10,
      "appliedPercent": 10,
      "capped": false
    }
  ]
}
```

| Field | Meaning |
| --- | --- |
| `quoteId` | Use this in `POST /v1/orders` or `GET /v1/quotes?id=`. |
| `status` | `open` until accepted or expired. |
| `totalMinor` | Sum of `finalUnitMinor * quantity`. |
| `expiresAt` | Absolute expiry, 15 minutes after creation. |
| `fxAsOf` | FX snapshot used for EUR or GBP conversion. |
| `capped` | True when any line was raised to the margin floor. |
| `lines` | Public line items. Upstream identifiers are omitted. |

## Read a quote

`GET /v1/quotes?id={quoteId}` returns the same public shape. This helper is **Preview**: it is
implemented but not yet in the published OpenAPI document. It requires `catalog:read` and
only returns quotes owned by the calling partner.

The target contract also describes `GET /v1/quotes/{id}`. Use the query form until that path is
published.

## Expiry and capped prices

- A quote expires 15 minutes after creation. An order with an expired quote returns
  `409 quote_expired`.
- `capped: true` means the partner's requested discount would breach the contribution-margin
  floor. `finalUnitMinor` was raised to the floor. Do not recompute the total from
  `requestedPercent`.
- A quote can be accepted once. A second order with the same quote returns
  `404 quote_not_found` because the quote is no longer `open`.

## What the quote does not do

- It does not reserve stock.
- It does not debit the wallet.
- It does not guarantee that the upstream product will still be available at order time.
- It does not lock the FX rate beyond its own expiry.

The order rechecks the current price and availability against the quote snapshot. A mismatch
returns `409 price_changed`, and no order is created and no funds are debited.

## One-call alternative

`POST /v1/orders` accepts `currency`, `items` and `expectedTotalMinor` instead of
`quoteId`. The API creates an internal quote, checks the expected total and proceeds. Use this
shape only when the agent has a clear mandate and a maximum total. See [Orders](/docs/orders).

## Errors

| Status | Code | Meaning |
| ---: | --- | --- |
| 400 | `validation_error` | Missing items, bad currency, non-integer quantity, more than 200 lines or more than 5,000 units. |
| 409 | `out_of_stock` | At least one offer is unavailable. |
| 409 | `quote_expired` | The quote is older than 15 minutes. |
| 503 | `fx_unavailable` | EUR or GBP rate missing or stale. |
| 429 | `rate_limited` / `quota_exceeded` | Quote limit reached. No quote row was created. |

## Related

- [Orders](/docs/orders) for the purchase step.
- [Wallet](/docs/wallet) for the funds that pay for an order.
- [Agent payment](/docs/agent-payment) for budgets and mandates.
