Idempotency
Network requests fail. Connections drop, timeouts trigger, your client retries — and sometimes the original request did succeed. To make retries safe, every mutation in the Seller API requires an idempotency key.
When it's required
Every endpoint that changes state requires an Idempotency-Key request header:
POST /v1/subscriptionsPOST /v1/subscriptions/{id}/extendPOST /v1/subscriptions/{id}/cancelPATCH /v1/discounts/{id}
Read endpoints (GET) don't need an idempotency key.
How it works
POST /v1/subscriptions/123/cancel
Authorization: Bearer ssk_live_...
Idempotency-Key: 0e9f8c8b-c3f4-4e1d-9b9a-1a2b3c4d5e6f- The first request with a given key is processed normally. The response (status + body) is recorded.
- Any subsequent request from the same API key using the same idempotency key replays the recorded response, without re-running the operation.
- After 24 hours the recorded response is dropped. After that, a new request with the same key will be processed again.
This means it's always safe to retry a mutation as long as you reuse the same idempotency key.
Generating keys
Use any value that is unique per logical operation in your system. UUIDs are the obvious choice:
import { randomUUID } from "node:crypto";
const idempotencyKey = randomUUID();Constraints:
- 1 to 255 characters.
- Any printable characters are allowed, but stick to ASCII (UUIDs, ULIDs, hex strings) for portability.
One key per logical action. Generate the key when you decide to perform the action, then reuse it for every retry of that action. Generating a new key on each retry defeats the whole point.
Conflicts
If you reuse a key with a different request body, the API returns 409 idempotency_conflict:
{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency-Key was already used with a different request body."
}
}To recover, generate a fresh key for the new request.
Scope
Idempotency keys are scoped to a single API key. Two different API keys (even on the same server) can use the same key string without conflict.
Example: a robust cancel
import { randomUUID } from "node:crypto";
const cancelSubscription = async (subscriptionId) => {
const idempotencyKey = randomUUID();
const attempt = async (n = 0) => {
const res = await fetch(
`https://subscord.com/api/v1/subscriptions/${subscriptionId}/cancel`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUBSCORD_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
},
);
if (res.ok) return res.json();
if (res.status >= 500 && n < 3) {
await new Promise((r) => setTimeout(r, 1000 * (n + 1)));
return attempt(n + 1);
}
throw new Error(`Cancel failed: ${res.status}`);
};
return attempt();
};Even if the network drops mid-call, the same idempotencyKey ensures the customer is only canceled once.