Idempotency

Networks lose responses. Without idempotency, a lost response leaves you guessing whether you just published a post. So every write requires a key.

The rule

Every POST, PUT and DELETE must carry an Idempotency-Key header. Omit it and the request is rejected with 400 idempotency_key_required — before anything happens.

curl -X POST "https://vm.viraldashboard.io/api/public/v1/brands/412/posts" \
  -H "Authorization: Bearer $VD_API_KEY" \
  -H "Idempotency-Key: 9f8c1e2a-4b77-4d31-9a10-2b6f5c0e7d44" \
  -H "Content-Type: application/json" \
  -d '{"content":"Hello","schedule_type":"draft"}'

Use any unique string — a UUID is the obvious choice. Generate it once, before the first attempt, and reuse that same value for every retry of that same logical operation. A key generated inside your retry loop defeats the entire mechanism.

The three outcomes

You sendYou get
A new key The request runs normally.
A key you already used, same body The stored response from the first attempt — same status, same body — with Idempotent-Replayed: true. The work is not done again.
A key you already used, different body 409 idempotency_error. Nothing happens. You are reusing a key for a different operation, which is a bug we would rather surface than paper over.

Replays are served for 24 hours after the original request. Past that the key is forgotten and reusing it will simply run the request again.

Why the 409 matters

Suppose you cache one key per user session and send two different posts with it. Without the conflict check, the second post would silently return the first post's response — you would think it published, and it never existed. The 409 tells you exactly that you have a key-reuse bug, and it costs you nothing: no post was created either way.

Retrying safely

import { randomUUID } from 'node:crypto'

async function publish(brand, body) {
  const idempotencyKey = randomUUID()          // ONCE — outside the retry loop

  for (let attempt = 1; attempt <= 4; attempt++) {
    const res = await fetch(`${process.env.VD_API}/brands/${brand}/posts`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.VD_API_KEY}`,
        'Idempotency-Key': idempotencyKey,     // the SAME key every attempt
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    })

    if (res.ok) {
      // Was this the real thing, or a replay of an earlier attempt that did land?
      if (res.headers.get('Idempotent-Replayed') === 'true') {
        console.log('a previous attempt had already succeeded')
      }
      return res.json()
    }

    if (res.status < 500 && res.status !== 429) throw new Error(await res.text())

    await new Promise(r => setTimeout(r, 2 ** attempt * 1000))
  }

  throw new Error('gave up after 4 attempts')
}

The timeout case is the one that matters. Your request reached us, the post published, and the response died on the way back. You retry with the same key, we recognise it, and you get the original 201 with Idempotent-Replayed: true. One post, not two.

Scope the key to the operation, not the resource. "Publish this post" is one operation and deserves one key. Later editing that post is a different operation and needs a different key.