Skip to content
← All articles
Stop Double-Posting: Idempotency for Social APIs

Stop Double-Posting: Idempotency for Social APIs

Every duplicate post starts the same way: a timeout, then a retry, then the same caption going live a second time, seconds after the first. The social network did nothing wrong. Your script sent the create call twice, and the platform did what it was told twice. Retries have to stay in your pipeline: a request can die between the server accepting it and your client reading the response. What has to change is the retry itself. PostSider’s public API lets you tag every create with an Idempotency-Key header on POST /public/v1/posts, and replaying the same key returns the original result instead of a duplicate. That one header is the difference between a no-op and an apology post. This is the recipe to follow before an agent burns a client account.

A double-post is a retry problem, and retries are not optional

A double-post is almost always a client retry after a timeout, a dropped connection, or a 500 response, not a defect in the social platform. Transient failures are common enough that retrying is mandatory, so the fix belongs in the retry itself: give it an identity the API can recognize, instead of sending an anonymous second create.

Walk through the failure sequence once and the duplicate stops looking mysterious. Your handler sends POST /public/v1/posts. The server accepts the request and creates the post. The response is lost: a proxy times out, the connection resets, or a 500 lands. Your client sees a failure, correctly retries, and sends the same request again with no way for the server to know it already processed one. Two posts, one intent. From the platform’s side there were two valid creates, so the platform is not the party that has to change. The anonymous retry is the bug. Give the retry an identity and the second call becomes a lookup instead of a create.

Social publishing is at-least-once: design for duplicates, not against them

Social posting APIs are at-least-once systems: a create request can be fully processed even when the client never receives the response, and the network decides when that happens. The honest target for posting is effectively-once, which client idempotency keys plus server-side dedup deliver, and no honest system can promise exactly-once.

You cannot design a posting pipeline against duplicates because you do not control the moment a response vanishes. Any boundary between two systems on a network is at-least-once by nature. So design for the duplicate. Give every create a stable key the server can index, and let the API answer the only question that matters: has it seen this request before? A client-side key plus server-side dedup is what people mean by effectively-once for creates, and it is the strongest guarantee on offer. It is also enough, because duplicates stop happening. Be suspicious of any tool or blog that promises exactly-once across social networks, and do not put that promise in your own agent.

An Idempotency-Key turns a retry from a risk into a no-op

POST /public/v1/posts creates a post for one or more channels in a single call and accepts an Idempotency-Key header. Replaying the same key for the same organization returns the existing result instead of creating a second post, and replaying a key with a different body returns HTTP 409 Conflict.

The call itself is ordinary, which is the point. All calls go to the public REST API v1 at https://api.postsider.com/public/v1 with an API key in the Authorization header:

curl -X POST https://api.postsider.com/public/v1/posts \
  -H 'Authorization: your-api-key' \
  -H 'Idempotency-Key: campaign-2026-07-01-001' \
  -H 'Content-Type: application/json' \
  -d '{"type": "schedule", "date": "2026-07-01T10:00:00Z", "posts": [{"integration": {"id": "ch_abc123"}, "value": [{"content": "Hello from PostSider!"}]}]}'

The key has to be stable for the logical post, or it does nothing. Derive it from something deterministic, like the campaign identifier and schedule date in the example above, so a retry after a crash reuses the same value. A fresh random UUID per attempt is an anonymous retry with extra steps. The 409 is the other half of the contract: replaying a known key with a different body means your script changed the content between attempts, and the API refuses to create anything rather than guess which version you meant. Get the key right and the retry becomes a no-op.

Check-before-create with list posts catches what the key cannot

GET /public/v1/posts accepts startDate and endDate query parameters in ISO 8601 UTC and returns a posts array with id, state, publishDate, and integration. That list is the check-before-create layer: a script can read the date range it is about to write into and skip creation when the intended post already exists.

The key protects retries inside one logical operation. It does not protect two separate runs of a batch job. If your process restarted and rebuilt its state, it might mint a new key for the same scheduled post, and the API will create a second one because it has never seen that key before. This is where the list earns its place:

curl -X GET 'https://api.postsider.com/public/v1/posts?startDate=2026-07-01T00:00:00Z&endDate=2026-07-02T00:00:00Z' \
  -H 'Authorization: your-api-key'

Before creating, list the range you are about to write into and compare id, state, publishDate, and integration against what you intended to schedule. If the post you meant to create is already there, skip the create. Keys make a retry safe. The list makes a fresh run of an old job safe. An agent that schedules on a timer needs both.

Webhooks retry too: confirm with the post id, verify the signature

A signed post.published webhook fires only on successful publish, with up to 3 total delivery attempts (immediate, then about 1 second, then about 2 seconds) on timeouts, network failures, and 5xx responses. After the third failed attempt the delivery is abandoned, and 4xx responses are logged without a retry, so consumers must treat events as at-least-once: dedupe on the post id and verify the HMAC signature.

Webhooks are notifications, not commands, so a duplicate delivery cannot create a duplicate post. It can run your recording logic twice, which is how dashboards and analytics end up with double counts. Every delivery carries X-Postsider-Event, X-Postsider-Timestamp in unix seconds, X-Webhook-Attempt counting 1 to 3, and X-Postsider-Signature. The signature is an HMAC-SHA256 of the timestamp, a dot, and the raw body, keyed with your webhook secret. The @postsider/node SDK wraps verification in one call:

import { Postsider } from "@postsider/node";

const SECRET = process.env.POSTSIDER_WEBHOOK_SECRET as string;
const MAX_AGE_SECONDS = 5 * 60;
const seenPostIds = new Set<string>();

export function handleWebhook(headers: Record<string, string>, rawBody: string): boolean {
  const signature = headers["x-postsider-signature"];
  const timestamp = Number(headers["x-postsider-timestamp"]);
  const event = headers["x-postsider-event"];

  if (!signature || !timestamp) return false;
  if (Math.floor(Date.now() / 1000) - timestamp > MAX_AGE_SECONDS) return false;
  if (!Postsider.verifyWebhookSignature(signature, rawBody, SECRET, timestamp)) return false;

  if (event !== "post.published") return false;

  const posts = JSON.parse(rawBody) as Array<{ id: string }>;
  for (const post of posts) {
    if (seenPostIds.has(post.id)) continue; // duplicate delivery, already recorded
    seenPostIds.add(post.id);
    // record the successful publish here
  }
  return true;
}

Reject timestamps older than five minutes. Fail closed on a bad signature. Then dedupe on the post id in the payload. A handler that does this can receive the same event three times and record it once. The full webhook walkthrough, with the retry timing spelled out, is at https://postsider.com/blog/webhooks-post-went-live.

Test by injecting failures: timeouts, 429s, and a handler that dies mid-run

The API allows 60 requests per minute per organization and reports the budget through X-RateLimit-Limit and X-RateLimit-Remaining on every call. On a 429 the body carries a retryAfter field, values like 30, plus a Retry-After header, so retry logic must be drilled against synthetic timeouts, 429 responses, and a process killed after the create call.

The lost response. Run your client against a wrapper that forwards the create and then drops the response, so the request lands but the answer never comes back. Replay the same key after the timeout, confirm you get the existing result, and confirm the platform shows exactly one post.

The rate limit. Fire requests until you hit 429. Watch X-RateLimit-Limit and X-RateLimit-Remaining drain, read retryAfter from the body and Retry-After from the headers, sleep that long, then retry with the same Idempotency-Key. A client that retries without backing off stays limited. One that retries with a fresh key turns a flood into duplicates the moment the limit clears.

The mid-run kill. Have your handler die after the create is sent but before the response is stored, then restart it. With a deterministic key you replay and get the original result. Without one, the date-range list is what catches the already-created post.

The rest of the error table deserves a read while you are there: 400 carries a per-channel validation message, 402 means a plan limit such as posts_per_month or channel, 413 means the payload is too large, and 409 is the key replay mismatch you now know how to read.

Idempotency is the cheapest reliability upgrade a posting pipeline can get, and it is entirely client discipline. Stable keys. A date-range check before creating. A webhook handler that verifies before it records. If you are building the scheduling side next, https://postsider.com/blog/schedule-posts-with-a-social-media-api walks through posting over the API end to end. The reference for everything above is at https://docs.postsider.com/api.

Lukasz Blania is the solo founder of PostSider.

Frequently asked questions

What is an Idempotency-Key?

A stable client-generated key sent with a create request. Replaying the same key returns the original result instead of creating a second post. PostSider returns 409 if the same key arrives with a different body.

Does PostSider support idempotent post creation?

Yes. POST /public/v1/posts accepts an Idempotency-Key header. Reusing the same key for the same organization returns the existing result, and the rate-limit guidance recommends a stable key for safe retries.

Can a webhook delivery create a duplicate post?

Webhooks are notifications, not commands. They retry up to three times on failures, so treat them as at-least-once: dedupe on the post id in the payload and verify the HMAC signature.

What should my script do on a 429?

Wait the Retry-After seconds, then retry with the same Idempotency-Key. Retrying without a key, or with a new one, is exactly how duplicates get created.

Run your social media
on autopilot.

Start free in minutes. Publish it yourself, or let your AI agent take the wheel.

30+ networks · MCP, REST and SDK · No credit card