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

# Requotes and expiry

> Ask a quote's venue again for a new price: to change the wallet, account, corridor or price protection, to get an executable price, or to refresh a quote that expired.

A requote asks the venue of a quote you already hold for a new price. You send the quote's id instead of a new search, and you get back one new quote from that venue. Every other venue is left alone.

> [!NOTE]
>
> The API may change until launch. Every change is listed in the [Changelog](https://tetrafi.mintlify.site/changelog.md).

## When to requote

* **To change how the quote executes.** Pick another trading wallet or venue account (`source`), another destination for the output (`deliverTo`), another corridor the quote offered (`corridor`), or a different price protection (`priceProtectionBps`). A corridor is the pair of chains a cross-chain quote moves between, from the chain you sell on to the chain you buy on; a quote lists the other corridors its venue offers in `corridors`.
* **To get an executable price.** An indicative quote is the venue's price, not a commitment. A requote asks the venue for a quote you can order. For a desk whose card reads `next.kind: "requote"`, this is how you get its firm price.
* **To refresh a price that expired.** An executable quote can be ordered only until its `expiresAt`. After that, requote it for a new price from the same venue.

## The request

`POST /api/v1/quotes`, the same endpoint as a search, with a different body:

| Field | Required | Value |
| - | - | - |
| `requote` | Yes | The quote id (`q_…`) from an earlier `POST /api/v1/quotes`. |
| `source` | No | A different trading wallet per chain (`source.tradingWallets`), venue account (`source.venueAccountId`) or funding account (`source.fundingAccountId`), each by the `financialAccountId` the answer's `sourceOptions` gave. Leave it out to keep the original's. |
| `deliverTo` | No | Where the output goes, for example `{"kind": "financialAccount", "financialAccountId": "..."}`. With an API key, a venue account's output stays on the account. |
| `corridor` | No | `{"from": "<chain>", "to": "<chain>"}`: another corridor the original quote listed in `corridors`. |
| `priceProtectionBps` | No | 0 to 1000. The price protection for the new quote's `minOut`, on a quote that is not firm. Leave it out to keep the original's. |

These four fields are the whole body. The pair, the amount, the side and the filters come from the original quote and cannot change: sending any of them with `requote` is refused with `400 REQUOTE_FIELDS_CONFLICT`. For a different pair or amount, search again.

```bash cURL theme={null}
# Ask the same venue again for the same pair and amount. The answer is a new quote whose
# requoteOf names the original. The original stays orderable until its own expiresAt.
requoted=$(quote "{\"requote\":\"$quote_id\"}")
fresh=$(jq -c '.quotes[0]' <<<"$requoted")
jq -c '{quoteId: .id, requoteOf, firm, buy: .buy.amount, minOut, expiresAt}' <<<"$fresh" >&2
```

```typescript TypeScript SDK theme={null}
// Requote by id. The answer is a new quote set; the refreshed quote carries a new id and `requoteOf`.
const fresh = await client.requote(held.id);
const renewed = fresh.quotes.find((quote) => quote.requoteOf === held.id);
if (!renewed) throw new Error('The requote answered without a quote naming the original in requoteOf.');
// Order `renewed.id` from here on, never the lapsed `held.id`.
```

```json Response (shortened) theme={null}
{
  "quotes": [
    {
      "id": "q_01M5XJXN…",
      "requoteOf": "q_01M5XJXM…",
      "firm": false,
      "buy": { "asset": "USDT", "amount": "5.00685" },
      "minOut": "4.99182",
      "expiresAt": "2026-10-27T02:27:15.792Z",
      "next": { "kind": "order" }
    }
  ]
}
```

What you get back:

* **One new quote** with its own `id`, whose `requoteOf` names the quote you requoted. Order the new id.
* **A JSON reply, never a stream**, even if you send `Accept: text/event-stream`.
* **The original stays orderable** until its own `expiresAt`. Requoting does not cancel it. Each quote can still be ordered only once.
* **Only that venue is asked.** For a route with several legs, every venue on the route is asked for that same route. If the venue declines, you get a named refusal, and TetraFi never substitutes another venue.

A requote is a live quote request, so it counts toward the limit of one live quote request every 3 seconds.

### Change the price protection

A non-firm executable quote carries `minOut`, the least it may deliver. `priceProtectionBps` sets how far below the quoted amount that floor sits. Here a requote at 50 basis points lowers `minOut` from `4.99182` to `4.98181` on the same quoted amount:

```bash cURL theme={null}
# Change the price protection for the new quote. A wider protection lowers minOut, the least
# a non-firm quote may deliver.
protected=$(quote "{\"requote\":\"$quote_id\",\"priceProtectionBps\":50}")
jq -c '.quotes[0] | {quoteId: .id, requoteOf, buy: .buy.amount, minOut}' <<<"$protected" >&2
```

## Quote times

| Field | Set on | Meaning |
| - | - | - |
| `expiresAt` | Executable quotes | When the quote stops being orderable. `null` on an indicative quote. |
| `staleAfter` | Indicative quotes | When the indicative price is old. `null` on an executable quote. |
| `observedAt` | Indicative quotes and order-book estimates | When the venue's price or order book was read. `null` on a firm quote. |
| `firmAskAvailableAt` | Desk quotes, when known | The workspace has reached the desk's firm-quote limit until this time, so a requote asking it for a firm price is refused before then. |

You can requote a quote until its `retainUntil`: 10 minutes after it expired (its `expiresAt`, or `staleAfter` for an indicative quote). Before then, ordering the lapsed quote answers `410 QUOTE_EXPIRED` with `next.kind: "requote"`. After it, a requote or an order answers `410 REQUOTE_WINDOW_CLOSED` with `next.kind: "search"`: search again with a pair body.

## Firm and non-firm quotes

`firm` and `expiresAt` say whether the venue is bound to its price:

| `firm` and `expiresAt` | Delivery | `minOut` |
| - | - | - |
| `firm: true`, `expiresAt` set | Exactly the quoted amount, until `expiresAt`. | `null` |
| `firm: false`, `expiresAt` set | May move, but never below `minOut`. | set |
| `firm: false`, `expiresAt` `null` | Indicative: not a commitment. | `null` |

`next` says what you do with the quote: `order`, `fund`, or `requote` (`preflight`, `sign` and `transact` appear only on Advanced quotes, which are not open yet). When it is `requote`, `next.needs` lists what the new quote needs from you, such as a trading wallet to name. Some desks' indicative quotes read `next.kind: "order"`: you order them with a minimum receive instead, as [Orders](https://tetrafi.mintlify.site/trade/orders.md#venue-quotes-that-execute-within-your-limit) explains.

## Refresh an expired quote

Ordering a quote after its `expiresAt` is refused with `410 QUOTE_EXPIRED` and nothing is sent. The refusal's `next` is `{"kind": "requote"}`, so requote the same id to get a fresh price from that venue:

```bash cURL theme={null}
# Ordering a quote after its expiresAt is refused with 410 QUOTE_EXPIRED and next.kind
# "requote". Requote it, up to 10 minutes after it expired, to get a fresh price from that venue.
expires=$(jq -r '.expiresAt' <<<"$first")
until [[ $(date -u +%Y-%m-%dT%H:%M:%S) > ${expires%.*} ]]; do sleep 1; done
if lapsed=$(tetrafi POST /api/v1/orders "{\"quoteId\":\"$quote_id\"}" \
  "Idempotency-Key: $(openssl rand -hex 16)" 2>/dev/null); then
  echo '{"error":"EXPECTED_QUOTE_EXPIRED"}' >&2
  exit 1
fi
jq -c '{error, next}' <<<"$lapsed" >&2
if [[ $(error_of "$lapsed") == QUOTE_EXPIRED ]]; then
  refreshed=$(quote "{\"requote\":\"$quote_id\"}")
  jq -c '.quotes[0] | {quoteId: .id, requoteOf, buy: .buy.amount, expiresAt}' <<<"$refreshed" >&2
fi
```

```json Output theme={null}
{"error":"QUOTE_EXPIRED","next":{"kind":"requote","needs":[]}}
{"quoteId":"q_01M5XJNK…","requoteOf":"q_01M5XJNB…","buy":"5.0068","expiresAt":"2026-10-27T02:22:49.076Z"}
```

## Refusals

A refused requote answers the standard error body: `error` is the code, `message` a sentence you can show, and `details` or `next` where present. For a `429`, wait the `Retry-After` header's seconds before asking again.

| Code | Status | Why | What to do |
| - | - | - | - |
| `REQUOTE_FIELDS_CONFLICT` | 400 | The body has a field that cannot go with `requote`, such as `sellAmount` or `filters`. | Send only `requote`, `source`, `deliverTo`, `corridor` and `priceProtectionBps`, or search again. |
| `CORRIDOR_NOT_OFFERED` | 400 | The original quote was not offered on that corridor. | Use a corridor from the quote's `corridors`. |
| `WALLET_NOT_ON_CHAIN` | 400 | The trading wallet you named is not an account on that chain. | Name a trading wallet on that chain in `source.tradingWallets`. |
| `SOURCE_NOT_OWNED` | 403 | A wallet or account in `source` or `deliverTo` is not one you can use. | Name a wallet or account from `sourceOptions`. |
| `ACCOUNT_NOT_AVAILABLE` | 403 | That venue account is not the one this quote was priced on. | Leave out `source.venueAccountId`, or search again. |
| `VENUE_NOT_AVAILABLE_TO_CREDENTIAL` | 403 | This credential cannot ask this venue for a firm quote. | Use a key with **Read quotes** and **Submit orders**, or a member session. |
| `QUOTE_NOT_FOUND` | 404 | No quote with this id is available to you. Another key's or member's quote answers the same. | Check the id. Requote only quotes your own key requested. |
| `PERSONAL_TARGET_NOT_ALLOWED` | 409 | An API key named a personal wallet. | Bind an organisation-scope wallet. |
| `REQUOTE_REFUSED` | 409 | The venue declined. `message` gives its reason in words and `details.reason` a token, for example `productNotQuoted` or `venueNotListed`. | Show `message`. Your original quote still holds until its expiry. Treat a reason you do not recognise as "other". |
| `REQUOTE_NEEDS_WALLET` | 409 | The venue quotes only once you choose a wallet. | Requote again with `source.tradingWallets`. |
| `QUOTE_OWNER_CONTEXT_UNKNOWN` | 409 | The quote was recorded before it could be refreshed for a client. | Search again. |
| `REQUOTE_WINDOW_CLOSED` | 410 | The quote is past its `retainUntil`. `next` is `search`. | Search again. |
| `QUOTE_RATE_LIMITED` | 429 | More than one live quote request in 3 seconds. | Wait `Retry-After` seconds, then send it again. |
| `VENUE_FIRM_QUOTE_LIMITED` | 429 | The workspace reached the venue's firm-quote limit. | Wait `Retry-After` seconds. The quote's `firmAskAvailableAt` says when, where known. |
| `BUDGET_EXHAUSTED` | 429 | The venue's quote budget is used up for now. | Wait `Retry-After` seconds when it is set, otherwise try later. |
| `QUOTE_NOT_YET_DURABLE` | 503 | The quote is still being recorded. | Send the same requote again after a second. |
| `QUOTE_STORE_UNAVAILABLE` | 503 | Held quotes cannot be read right now. | Try again shortly. |
| `REQUOTE_TIMED_OUT` | 504 | The venue did not answer in time. | Your original quote still holds until its expiry. Try again, or order the original. |

## In the app

On the Trade page, an expired quote card offers to request it again, a desk's indicative card offers **Get firm quote**, and changing price protection in Review offers **Requote and execute**: each sends this same requote.


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