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

# Idempotency

> The Idempotency-Key header on the listed POST requests, the key rules, the replay, the two conflicts and a retry recipe.

<Note>
  The Marketplace API is in a private beta. It answers only for accounts Investorlift has enabled. [Access](/marketplace/access) says how to ask for one. A route shape on these pages can change before the beta ends. The changelog records every change.
</Note>

A network fault can lose an answer after the write went through. So every `POST` this page lists takes an
`Idempotency-Key` header, and a repeat with the same key is safe. The API runs the write once and answers
the same body to every repeat.

## Which requests take the key

* The seller's creating and moving posts: `POST /sell/drafts`, `POST /sell/drafts/{draft_id}/publish`,
  `POST /sell/deals/{deal_id}/status`, `POST /sell/deals/{deal_id}/offers`, `POST /sell/deals/{deal_id}/leads`,
  `POST /sell/buyers/{buyer_id}/strikes` and `POST /sell/reviews/{review_id}/response`.
* The seller's answers to an offer and to an address request: `POST /sell/offers/{offer_id}/accept`, `/decline` and
  `/counter`, and `POST /sell/inquiries/{inquiry_id}/approve` and `/decline`.
* The buyer's writes: `POST /buy/offers`, `POST /buy/offers/{offer_id}/counter`, `/decline`, `/accept` and `/withdraw`,
  `POST /buy/deals/{deal_id}/inquiries`, `POST /buy/buy-boxes` and `POST /buy/me/proof-of-funds`.
* The webhook endpoints of both sides, `POST /sell/webhooks` and `POST /buy/webhooks`.

The reference lists `409 idempotency_conflict` and `409 idempotency_in_progress` on each of these operations, and on
no other operation.

A preview (`POST .../offers/preview`, `POST .../counter/preview`) writes nothing and takes no key. An upload ticket
writes nothing. A media post (`POST .../media`) and a document post (`POST .../documents`) take no key. A repeat adds
the same staged file a second time, so send each staged key once. `POST /sell/deals/{deal_id}/reassign` and the webhook
test posts (`POST .../webhooks/{webhook_id}/test`) take no key. A `PATCH` sets a state and takes no key.

One of the posts in the list above without the header answers `400 invalid_parameter` with
`parameter: "Idempotency-Key"`.

## The key

* 1 to 255 characters. Use a UUID, or a key of your own system, for example your record id and the action.
* Scoped to your OAuth client and to the person the token names. The same string from another client or another person
  is another key.
* Kept 24 hours. After that, the same string starts a new request.
* Stored with a fingerprint of the method, the path and the body. The order of the keys inside the JSON does not change
  the fingerprint.

## Replay and the two conflicts

| You send                                                         | The API answers                                                                                         |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| The same key and the same body, after the first request finished | The stored status and body, with the header `Idempotent-Replayed: true`. The write ran once.            |
| The same key and another body                                    | `409 idempotency_conflict`. Send a new key for the new body.                                            |
| The same key while the first request still runs                  | `409 idempotency_in_progress` with `Retry-After: 1`. Wait one second, then send the same request again. |
| The same key after a `5xx`                                       | The API released the key with the fault. The same key runs the write again.                             |

The API checks your authorization again before a replay. The replayed body is the first answer, byte for byte, so its
`meta.request_id` is the first request's. The `X-Request-Id` header is the new request's.

## The domain rules

Beyond the key, three rules of the domain refuse a duplicate:

* One active deal per property. A second publish on an address with a live deal answers `409 address_unavailable`.
* One active chain per buyer per deal. A second submit answers `409 offer_exists`, and the body names the open round.
* One inquiry per type per deal per day. The second inquiry, or the second address request, on one deal in 24 hours
  answers `429 duplicate_inquiry` with `Retry-After: 86400`, and the body names `deal_id` and `type`.

A repeat of a counter on a round that closed answers `409 offer_superseded`, with a key or without one.

## A retry recipe

<Steps>
  <Step title="Mint the key before the first send">
    Make the key once and store it with the intent, then send. A key made after a failure protects nothing.
  </Step>

  <Step title="Repeat the same request on a network fault or a 5xx">
    Same method, same path, same body, same key. Wait 1, 2, 4 and 8 seconds between tries. Stop after five tries and raise
    an alert with the key.
  </Step>

  <Step title="Read the answer">
    A `2xx` with `Idempotent-Replayed: true` is the first answer again. On `409 idempotency_in_progress`, wait `Retry-After`,
    then repeat. On `409 idempotency_conflict`, stop: your body changed between tries. On any other `4xx`, change the request
    and use a new key.
  </Step>
</Steps>

```javascript Node.js theme={null}
import { randomUUID } from "node:crypto";

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function submitOffer(body) {
  const key = randomUUID(); // minted once, before the first send
  for (let attempt = 0; attempt < 5; attempt++) {
    let res;
    try {
      res = await fetch("https://api.investorlift.com/marketplace/v1/buy/offers", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.MARKETPLACE_TOKEN}`,
          "Content-Type": "application/json",
          "Idempotency-Key": key,
        },
        body: JSON.stringify(body),
      });
    } catch {
      await sleep(1000 * 2 ** attempt); // a network fault: same key, same body
      continue;
    }
    if (res.status === 409) {
      const problem = await res.clone().json();
      if (problem.code === "idempotency_in_progress") {
        await sleep(1000 * Number(res.headers.get("Retry-After") ?? "1"));
        continue;
      }
    }
    if (res.status >= 500) {
      await sleep(1000 * 2 ** attempt); // the API released the key
      continue;
    }
    return res; // a 2xx, a replay, or a 4xx to act on
  }
  throw new Error(`no answer after five tries for key ${key}`);
}
```

[Errors](/marketplace/concepts/errors) has both conflict codes.
[Identifiers and the envelope](/marketplace/concepts/identifiers-and-envelope) has the request id.


## Related topics

- [Errors](/marketplace/concepts/errors.md)
- [Quickstart for sellers](/marketplace/quickstart-seller.md)
- [Quickstart for buyers](/marketplace/quickstart-buyer.md)
- [Identifiers and the envelope](/marketplace/concepts/identifiers-and-envelope.md)
- [Finish a proof of funds upload](/api-reference/buy/finish-a-proof-of-funds-upload.md)
