Key takeaways
- X gives its duplicate-content refusal no distinct error code — it has to be matched on the message text (X has reworded it more than once, so a robust matcher checks several known substrings, e.g. "duplicate content", "duplicate tweet", "already tweeted").
- The refusal is widely reported to fire AFTER the write has landed: the tweet gets created, X's duplicate-content check runs, and the caller receives a 403 for a post that is already live.
- Bucketing it with ordinary malformed-request 403s tells the caller the post never happened — and a Retry button then posts it a second time.
- The only safe resolution is to read the account's recent posts back and check whether the content is already there, never to resend the create call.
Twitter API 403 duplicate content: why this is not a validation error
Most 403s from a publish endpoint mean exactly what they say: the request was refused, nothing was created, and the safe response is to tell the caller their post failed. X's duplicate-content 403 breaks that assumption. It is reported to sometimes fire AFTER the tweet has already been created — the write lands, X's own duplicate-content check runs against the newly created post, and the API answers with a 403 for a post that is, at that moment, already live on the timeline.
That makes it the one genuinely indeterminate outcome this endpoint can return: not "it failed", not "it succeeded", but "unknown, and re-sending the same content risks creating a second, real duplicate".
How to detect it
X does not give this refusal its own distinct error code, so detecting it means matching on the message text — and being loose about it, because X has reworded the message more than once. A matcher that only recognizes one exact string will miss a reworded version and fall back to treating it as an ordinary rejected request, which is the less safe direction. Matching several known substrings ("duplicate content", "duplicate tweet", "already tweeted") and over-recognizing this 403 is the safer failure mode than under-recognizing it: over-recognizing it means running one extra reconciliation check on a request that actually did just fail cleanly; under-recognizing it means silently risking a double-post.
The only safe resolution: read the timeline back
Once the 403 is recognized as the duplicate-content case, the only safe move is to read the account's recent posts back (GET /2/users/:id/tweets) and check whether the attempted content is already there, published moments ago. If it is found, that is the real, successful id — return it as a normal success rather than as a failure, so the row gets the true post id and nothing downstream tries to send it again.
If the reconcile does NOT find it, that is weaker evidence than it sounds — a timeline read can lag by seconds, and a post whose visibility was restricted might not show up in that listing at all — so the correct outcome is still not "it failed": it is "unknown", surfaced as a state a human resolves, never one a Retry button silently re-sends into a duplicate.
A scope dependency worth calling out
This reconciliation only works if the integration still holds read access to the account's own timeline. An app that requests only a write-post scope, on the theory that reading isn't needed for a publish-only tool, loses the one mechanism that makes this specific 403 recoverable — with no read access, the indeterminate outcome can never be resolved automatically, and every occurrence becomes a manual investigation.
FAQ
- Does X give the duplicate-content 403 its own distinct error code?
- No — it has to be matched on the message text, and the text has changed more than once, so a robust check matches several known phrasings rather than one exact string.
- Can this 403 really arrive after the post is already live?
- Yes, that is the specific reason it needs different handling than an ordinary rejected request — the create call can succeed and the duplicate-content check can still return a 403 for a post that already exists on the timeline.
- Is it safe to just retry the post when this error appears?
- No. If the original attempt actually succeeded, retrying creates a second, genuinely duplicate post — the safe response is to check the timeline for the content first, not to resend it.
- What if reading the timeline back does not find the post either?
- Treat that as unknown, not as failed — a timeline read can be stale by a few seconds and a visibility-restricted post might not appear in it, so the honest outcome is an indeterminate state a person resolves, not an automatic retry.
- What OAuth scope does reconciling this error require?
- A read scope on the account's own posts, in addition to the write scope used to publish — a write-only integration cannot read the timeline back and loses the only safe way to resolve this specific error.
- Does this affect every 403 from X's post-creation endpoint?
- No — most 403s from that endpoint are ordinary rejected requests where nothing was created. Duplicate-content is the specific, narrower case that behaves differently and needs the reconcile-before-retry handling described here.