Idempotency
Safely retry post creation with an Idempotency-Key so a network failure or timeout can never result in a duplicate post.
When a request times out or the connection drops, you cannot tell whether the server processed it. Retrying is risky - the post may already have been created and published. Not retrying is also risky, because the post may never have been created at all.
Idempotency keys remove the guesswork. Send a key with your request and you can retry it as many times as you like: the first attempt does the work, and every later attempt with the same key returns that first attempt's result instead of creating another post.
Supported endpoints
| Endpoint | Idempotency |
|---|---|
POST /v1/posts | Supported |
Other endpoints ignore the header today. GET and DELETE requests do not need it - they are already safe to repeat.
Sending a key
Add an Idempotency-Key header. Any printable ASCII string up to 255 characters works, but a UUID v4 is the right choice because it is collision-free without any coordination on your side.
curl -X POST https://api.outstand.so/v1/posts \
-H "Authorization: Bearer $OUTSTAND_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: b1f6c3f4-8f0e-4a5b-9c2d-7e1a0b3c4d5e" \
-d '{
"content": "Shipping something new today.",
"accounts": ["x", "linkedin"]
}'Generate the key once per logical operation, not once per HTTP attempt. All retries of the same create must carry the same key, otherwise each one is a new request and you are back to duplicating posts.
// Correct: the key is created outside the retry loop.
const idempotencyKey = crypto.randomUUID();
for (let attempt = 0; attempt < 3; attempt++) {
const response = await fetch('https://api.outstand.so/v1/posts', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(payload),
});
if (response.status === 409) {
// An earlier attempt is still being processed. Wait and retry the same key.
await new Promise((resolve) => setTimeout(resolve, Number(response.headers.get('Retry-After') ?? 1) * 1000));
continue;
}
return response;
}Requests without the header behave exactly as before, so adding keys is a change you can roll out gradually.
Replays
When a key has already produced a result, the API returns that stored response verbatim - same status code, same body - along with a header marking it as a replay:
HTTP/1.1 200 OK
Idempotency-Replayed: true
Idempotency-Key: b1f6c3f4-8f0e-4a5b-9c2d-7e1a0b3c4d5eIf you are migrating from Stripe, note the spelling. Stripe sends Idempotent-Replayed; Outstand sends Idempotency-Replayed, matching the request header name.
A replayed body is reproduced from stored JSON, so the order of its keys is not guaranteed to match the original response. Read fields by name rather than relying on ordering.
Retention
Keys are remembered for 24 hours from first use, and are scoped to your organization and to the endpoint. Two things follow from this:
- Every API key belonging to the same organization shares one key namespace. A retry sent with a newly rotated API key still deduplicates correctly.
- After 24 hours the key is forgotten. Reusing it creates a new post rather than replaying the old one, so keys are not a long-term record of what you sent.
Error responses
Every response below carries a machine-readable code alongside the human-readable error.
409 - a request with this key is still running
{
"success": false,
"error": "A request with this Idempotency-Key is already in progress. Retry in a moment.",
"code": "idempotency_request_in_progress"
}Your previous attempt has not finished yet. Wait for the interval in the Retry-After header and send the same key again; you will get the original result once it completes.
422 - the key was reused with a different body
{
"success": false,
"error": "This Idempotency-Key was already used with a different request body. Use a new key for a different request.",
"code": "idempotency_key_reuse"
}A key is bound to the exact request that claimed it, so it can never return a result for content you did not send. Reformatting your JSON or reordering its keys is fine - only a genuine change to the values counts as a different request. Use a fresh key for a different post.
400 - the key itself is unusable
{
"success": false,
"error": "Idempotency-Key must be at most 255 characters",
"code": "invalid_idempotency_key"
}Returned for an empty key, a key over 255 characters, or one containing control characters.
Failures that already created a post
One case deserves attention because retrying is the wrong response to it.
A post is stored first and queued for publishing immediately afterwards. If the queueing step fails, the post exists but is not scheduled, and you receive:
{
"success": false,
"post": { "id": "Xk3p9m", "scheduledAt": "2026-08-01T12:00:00.000Z", "...": "..." },
"scheduled": false,
"error": "Failed to schedule publishing task"
}Because a post was created, this response is stored and replayed like any other result. Retrying with the same key returns this same error, not a new post - which is the point: retrying cannot leave you with two posts.
To resolve it, act on the post in the response rather than retrying the create:
PATCH /v1/posts/{id}with ascheduledAtvalue to queue it again, orDELETE /v1/posts/{id}to discard it and start over with a new key.
If a request is interrupted before the post is stored, nothing is persisted and the key is released, so a straightforward retry with the same key succeeds normally.
If a request is interrupted after the post is stored — for example the connection dropped before the response reached you — a retry with the same key reports the post that exists rather than creating another one. Should that post have never been queued for publishing, the retry returns the same scheduled: false response above, so the resolution is again to PATCH or DELETE it. Retrying never queues publishing on your behalf, because a retry cannot tell "the task was never created" apart from "the task was created but the confirmation was lost", and guessing wrong would publish twice.