> ## 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.

# Idempotency

> Retry an order safely: one Idempotency-Key per order, replays that return the first answer, and how to find out whether an order was placed.

A network call can fail after TetraFi received it: a timeout, a dropped connection, a crash between sending and reading the answer. An `Idempotency-Key` makes the retry safe, so you never place the same order twice.

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

## Send a key with every order

An API key must send an `Idempotency-Key` header with every `POST /api/v1/orders`. Without one, the order is refused with `400 IDEMPOTENCY_KEY_REQUIRED`.

* Generate a new random value for each order, such as 32 hexadecimal characters.
* Send exactly one value, 1 to 255 bytes, with no spaces around it. Anything else is refused with `400 IDEMPOTENCY_KEY_INVALID`.
* Store the key with your record of the order before you send the request, so a retry after a crash can reuse it.

Keys are scoped to your workspace and to the endpoint.

```bash cURL theme={null}
# Generate one Idempotency-Key per order and store it with the order before you send it.
idempotency_key=$(openssl rand -hex 16)
order_body="{\"quoteId\":\"$order_quote\"}"
placed=$(tetrafi POST /api/v1/orders "$order_body" "Idempotency-Key: $idempotency_key")
# A basicTrade names its venue trade in operation; an advancedTrade names its order in order.
jq -c '{kind, quoteId} + if .kind == "basicTrade" then {operation: (.operation | {connectionId, operationId})}
  else {order: (.order | {orderId})} end' <<<"$placed" >&2
```

## Replay: same key, same body

Send the same body with the same key again and you get the first answer back. No second order is placed. The body is compared by its content, so whitespace and key order do not matter.

```bash cURL theme={null}
# Same key, same body: you get the first answer back and no second order is placed.
# Use this to retry after a timeout or a dropped connection.
replayed=$(tetrafi POST /api/v1/orders "$order_body" "Idempotency-Key: $idempotency_key")
jq -n -c --argjson a "$placed" --argjson b "$replayed" \
  '{replayedSameOrder: ($a.orderId == $b.orderId)}' >&2
```

```typescript TypeScript SDK theme={null}
// Sending the same request with the same key again returns the same order; it never places a second one.
const replay = await client.order(request, { idempotencyKey });
const id = (result: OrderResult): string => (result.kind === 'basicTrade' ? result.operation.operationId : result.order.orderId);
```

```json Output theme={null}
{"replayedSameOrder":true}
```

This holds even after the quote expired: a replay of an order that was placed returns that order, never `QUOTE_EXPIRED` or `QUOTE_ALREADY_ORDERED`.

## Conflict: same key, different body

Reusing a key with a different body is refused with `409 IDEMPOTENCY_CONFLICT`, and nothing is placed. Here the same quote with a `minReceive` added counts as a different body:

```bash cURL theme={null}
# Same key, different body: refused with 409 IDEMPOTENCY_CONFLICT. Nothing is placed.
changed="{\"quoteId\":\"$order_quote\",\"minReceive\":\"1000000\"}"
if conflict=$(tetrafi POST /api/v1/orders "$changed" "Idempotency-Key: $idempotency_key" 2>/dev/null); then
  echo '{"error":"EXPECTED_IDEMPOTENCY_CONFLICT"}' >&2
  exit 1
fi
echo "{\"reusedKeyWithNewBody\":\"$(error_of "$conflict")\"}" >&2
```

```json Output theme={null}
{"reusedKeyWithNewBody":"IDEMPOTENCY_CONFLICT"}
```

A conflict means your code reused a key by mistake. Use a new key for a new order.

## When to retry with the same key

| Answer | What it means | What to do |
| - | - | - |
| No answer (timeout, dropped connection) | The order may or may not have been placed. | Send the same body with the same key. |
| `503 IDEMPOTENCY_UNAVAILABLE` | The key could not be checked, so the request did not run. | Send the same body with the same key. |
| `409 ORDER_IN_PROGRESS` | An order with this key is still being placed. | Wait a few seconds, then send the same body with the same key. |
| `503 QUOTE_NOT_YET_DURABLE` | The quote is still being recorded. | Send the same body with the same key after a second. |
| `409 IDEMPOTENCY_CONFLICT` | The key was already used for a different body. | Fix your code. Do not send this body with a new key until you have [recovered by quote](#recover-an-order-by-its-quote). |

If you have lost the key itself, recover by quote.

## Recover an order by its quote

`GET /api/v1/orders/by-quote/{quoteId}` tells you what a quote produced. It never places anything, and it answers whether or not the quote has expired. The key needs **Read orders**.

```bash cURL theme={null}
# When you cannot tell whether an order was placed, read it by its quote id before doing
# anything else. ordered: read the order at statusUrl. notOrdered: nothing was placed.
# unknown: wait, then read again.
recovered=$(tetrafi GET "/api/v1/orders/by-quote/$order_quote")
jq -c '{state, orderId, statusUrl}' <<<"$recovered" >&2
unordered=$(tetrafi GET "/api/v1/orders/by-quote/$quote_id")
jq -c '{state, expired}' <<<"$unordered" >&2
```

```json theme={null}
{"state":"ordered","orderId":"acb84378-…","statusUrl":"/api/v1/orders/acb84378-…"}
{"state":"notOrdered","expired":false}
```

| `state` | What it means | What to do |
| - | - | - |
| `ordered` | An order exists. `orderId` is its id and `statusUrl` is where to read it. | Read that order's status ([Order status](https://tetrafi.mintlify.site/trade/order-status.md)). Do not order again. |
| `notOrdered` | No order was placed with this quote. `expired` says whether it can still be ordered. | If `expired` is `false`, you can order it. If `true`, requote it or search again. |
| `unknown` | TetraFi cannot yet prove either way. | Wait and read again. Do not order again. |

An unknown quote id, or one from another workspace, answers `404 QUOTE_NOT_FOUND`. If the read itself fails, it answers `503 QUOTE_ORDER_UNAVAILABLE`: do not place another order until a read succeeds.

## What never to do

* **Never retry an order whose outcome is unknown with a new key.** A new key is a new order. Retry with the same key, or read by quote first.
* **Never reuse a key for a different order.** It is refused, and it hides which order the key belongs to.
* **Never treat `unknown` or a failed read as "not ordered".** Only `notOrdered` means nothing was placed.
* **Never take `QUOTE_ALREADY_ORDERED` as a failure.** It means another order, sent with a different key, already used the quote. Read by quote to find it.

## In the app

If the connection drops while you place an order in the app, pressing again replays the same order with the same key, so it is never placed twice.


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