Guides

Idempotency

Make retries safe so a network timeout can never publish the same post twice.

A request that times out tells you nothing about whether it was applied. The post may have been created and the response lost, or the request may never have arrived. Retrying is the only reasonable response, and without idempotency, retrying is how you publish the same thing twice.

Send an Idempotency-Key header on writes:

curl -X POST https://api.cutedyno.com/v1/posts \
  -H "Authorization: Bearer $CUTEDYNO_API_KEY" \
  -H "Idempotency-Key: 8f14e45f-ea0c-4d7a-9f1a-2b3c4d5e6f70" \
  -H "Content-Type: application/json" \
  -d '{"content":"Shipping today.","accountIds":["…"],"publishNow":true}'

If that exact request already succeeded, CuteDyno returns the stored response instead of doing the work again, with an Idempotency-Replayed: true header so you can tell.

What the key covers

Keys are scoped to the API key that used them and stored for 24 hours. Within that window:

SituationWhat happens
First request with this keyExecuted normally, and the response is stored.
Same key, same bodyThe stored response is replayed. Nothing runs twice.
Same key, different body409 with code idempotency_key_reused.
Same key, but the first attempt failedExecuted again. Failures are not stored.
Key older than 24 hoursTreated as new.

That third row is a guardrail, not an inconvenience. If the same key arrives with a different body, one of the two requests is not what you meant, and replaying an unrelated response would be worse than an error.

The fourth row is what makes retry loops work: a 500 or a 429 leaves no stored response, so retrying with the same key genuinely retries.

Which endpoints accept it

Every POST, PUT, PATCH, and DELETE. It matters most on the ones that cost something:

GET requests ignore the header. They are already idempotent.

Choosing a key

Any string works, but the choice decides what the key protects you from.

A random UUID per attempt protects against nothing. Each retry looks like a new request.

A random UUID per logical operation, generated once and reused across retries, protects against retries within that operation. This is what the Node SDK does automatically: one key per call, reused across its internal retries.

// Generated once, reused across the SDK's own retries.
await cutedyno.posts.create({ content: 'Shipping today.', accountIds });

A key derived from your own data protects against everything, including a crash between the request and recording its result, and a job that runs twice:

await cutedyno.posts.create(
  { content: post.body, accountIds: post.accountIds, scheduledAt: post.scheduledAt },
  { idempotencyKey: `post-${post.id}` }
);

Now the operation is safe no matter how many times your worker runs it. If your scheduler fires twice, or a deploy restarts a process mid-flight, the second attempt replays rather than publishes.

This is the pattern to use for anything driven by your own database rows. The one thing to watch: the body must be identical, or you get idempotency_key_reused. If the row can change between attempts, include a version in the key — post-${post.id}-v${post.revision}.

A retry loop worth copying

import { CuteDynoError } from '@cutedyno/node';

async function createPostWithRetry(input: PostInput, key: string) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    try {
      return await cutedyno.posts.create(input, { idempotencyKey: key });
    } catch (error) {
      const retryable = error instanceof CuteDynoError && error.isRetryable;
      if (!retryable || attempt === 3) throw error;
      await sleep(error.retryAfter ? error.retryAfter * 1000 : 2 ** attempt * 500);
    }
  }
}

Three things make it correct: the key is created outside the loop, only retryable errors are retried, and Retry-After is honoured when present. The SDK already does all three; this is what to write if you are not using it.

Common mistakes

Regenerating the key inside the retry. The most common one. Each attempt then looks new, and idempotency does nothing.

Reusing one key for a batch. Ten posts under one key means one post and nine replays of its response. One key per post.

Assuming a replay means failure. Idempotency-Replayed: true on a 201 means the post exists. Treat it as success.

Deriving keys from a timestamp. post-${Date.now()} is a random key with extra steps.