Retries and duplicate posts: retry safely
A social post that goes out twice is worse than one that fails. This page says what OmniSocials does when a request or a publish goes wrong, and how to retry without posting twice.
The short version
- The API has no idempotency keys. Every
POST /posts/createmakes a new post, even with the same body. - After a timeout or a
5xxon create, checkGET /postsbefore you send the request again. - Retry failed channels with
POST /posts/:id/retry, never by creating the post again. 429and503 storage_unavailabledid nothing, so they are safe to resend afterRetry-After.- One post can send several webhook events. Track state per
data.post_id.
No idempotency keys
The API does not read an Idempotency-Key header or anything like it, and POST /posts/create does not compare a new post with earlier ones. Two identical requests create two posts, and when both publish, both go out (unless the platform refuses the second, see Duplicate content).
Media uploads are the exception. An image or video up to 100 MB is matched by its content: uploading the same bytes to the same workspace again returns the existing media item instead of a new one. This covers POST /media/upload, upload-from-base64, presigned uploads, upload-from-url and media_urls on a post. A file over 100 MB, ingested in the background through upload-from-url, creates a new item on every call.
Timeouts and 5xx errors
POST /posts/create saves the post before it builds the response. A 500 internal_error or a dropped connection can therefore arrive after the post was saved, and a saved post with publish_now: true publishes anyway. Treat a timeout or a 5xx on create as "unknown", not as "failed".
To make the check easy, give each create a unique source value. source is a free-form label (it defaults to "api", is shown in the dashboard and comes back on every post):
Terminal
If that call times out or answers 5xx:
- Wait a few seconds, then call
GET /posts?limit=20. Posts come back newest first. - Look for a post whose
sourceisspring-sale-job-8f3a. - If it is there, do not create again. Poll
GET /posts/:idfor its outcome. - If it is not there, send the create again with the same
source.
The other write endpoints are safer to repeat:
| Endpoint | What a second identical call does |
|---|---|
POST /posts/:id/publish | Only draft and scheduled posts can be published. A second call answers 400 invalid_status (the post is posting or warning), 400 already_published or 400 post_failed, and publishes nothing |
POST /posts/:id/retry | While the retry runs the post is posting, so a second call answers 400 invalid_status |
DELETE /posts/:id | The second call answers 404 not_found |
| Media uploads up to 100 MB | Returns the same media item |
POST /inbox/conversations/:id/reply | The same text and attachment to the same conversation within 10 minutes answers 409 duplicate_reply. Nothing is sent again, and data carries the earlier message. Treat it as sent |
Any request answered 429 | Rejected before it ran. Resend after Retry-After |
Any upload answered 503 storage_unavailable | Nothing was stored. Resend after Retry-After |
How OmniSocials retries a publish
Publishing runs in the background after your request returns. Several guards keep a channel from getting the same post twice:
- One success per channel. A channel's success is recorded the moment the platform confirms it. Every later run of the same post (a retry, or recovery after a crash) skips the channels that already succeeded.
- Lookup before resend. When Instagram's publish call or Facebook's feed post call times out or answers
5xx, OmniSocials first looks on the account for a post created after the attempt with the same caption, and keeps it when it finds one. Instagram sometimes answers "Action blocked" for a post that is already live; the same lookup runs then. A post without a caption, such as a story, cannot be matched this way. - Unknown outcomes are reported, not retried. When X accepts a post but returns no post id, the channel fails with "X accepted the request but did not return a post id, so we can't confirm the post went live. Check your X profile before retrying, to avoid a duplicate."
- Threads and stories resume. A Threads thread that fails halfway resumes at the first unpublished part, and a multi-slide story publishes only the slides that did not go out.
Automatic retries
A failure that OmniSocials classifies as temporary gets an automatic retry about 15 minutes later, up to 3 automatic attempts per channel. While one is planned, the channel's message in errors ends with "We'll retry this platform automatically in about 15 minutes."
The post's status is failed or warning in the meantime. Calling POST /posts/:id/retry yourself replaces the planned automatic retry. Automatic attempts are counted apart from the 3 manual retries per channel.
warning and failed
| Status | Meaning | Webhook |
|---|---|---|
warning | Published on some channels, failed on others. published_urls has the live links, errors the failed channels | post.published with data.status: "warning" |
failed | Failed on every channel | post.failed |
An errors entry disappears once a retry of that channel succeeds. GET /posts?status=failed does not return warning posts, so query both when you track failures. See Post statuses.
Retrying failed channels
POST /posts/:id/retry publishes the failed channels of a failed or warning post again, on the same post. Channels that already published are never posted again.
Terminal
JSON
The retry runs in the background. Poll GET /posts/:id until status is no longer posting.
| Response | Meaning |
|---|---|
400 invalid_status | The post is not failed or warning |
400 nothing_to_retry | No failed channel is recorded, for example on posts from before per-channel tracking. Create a new post instead |
400 max_retries_reached | A failed channel was already retried 3 times. All failed channels retry together, so one exhausted channel blocks the whole request. Create a new post |
Channels dismissed with "ignore" in the dashboard are not retried. POST /posts/:id/publish refuses failed posts (400 post_failed) and warning posts (400 invalid_status); retry is the endpoint for them.
Fix the cause first. A retry sends the same post again, so it fails again when the cause is the content or the account: a duplicate, a missing permission, an expired connection. Reconnect the account, top up credits, or change the post first. Known issues lists the common causes per channel. A warning post can still be edited with PATCH /posts/:id before you retry; a failed post cannot (400 invalid_status), so create a new post when its content has to change.
Retries made in the dashboard can create a separate retry post, linked with retry_of and retries. See Retry linkage.
Duplicate content
OmniSocials does not refuse a post for being the same as an earlier one. Some platforms do:
| Channel | Message |
|---|---|
| "Facebook rejected this as a duplicate" | |
| "LinkedIn rejected this post as a duplicate: identical content already exists on your profile" (or page) | |
| X | "This is a duplicate of a tweet you already posted." |
These refusals also cover content that was posted outside OmniSocials. A retry with the same text fails the same way: change the text first.
Webhooks for one post can arrive more than once
Every delivery carries X-OmniSocials-Delivery, a unique id per event that stays the same when a delivery is retried. Deduplicate deliveries on it.
One post can still produce several events, each with its own delivery id:
post.failedafter the first attempt, thenpost.publishedwhen a retry (manual or automatic) succeeds.post.publishedwithdata.status: "warning"after the first attempt, thenpost.publishedagain after each retry.- A planned automatic retry sends nothing until it ends.
Keep your state per data.post_id, read data.status and data.targets on every event, and order events by created_at. See Webhooks.
Checklist
| Situation | Resend? | What to do |
|---|---|---|
429 rate_limit_exceeded | Yes | Wait Retry-After seconds |
503 storage_unavailable | Yes | Wait Retry-After seconds, send the same request |
400, 401, 402, 403, 404 | No | Fix the request, key, scope or credits first |
Timeout or 5xx on POST /posts/create | Only after a check | Look for the post's source in GET /posts; create again only when it is missing |
Timeout or 5xx on publish or retry | Yes | A refusal with invalid_status, already_published or post_failed means the first call went through. Poll the post |
A channel failed (warning or failed) | Use retry | Fix the cause, then POST /posts/:id/retry |
409 duplicate_reply | No | The reply was already sent |
For the full status code table and the error body, see Rate limits and errors.