Idempotency & Safe Retries
How Zernio prevents duplicate posts, the x-request-id header, content-hash dedup, and the retry pitfalls in workflow tools like n8n and Zapier
Network blips, timeouts, and automatic retries in workflow tools (n8n, Zapier, custom queues) all end the same way: the same create-post request arrives twice. Two independent layers of duplicate protection make POST /v1/posts safe to retry.
Layer 1: x-request-id (same-request idempotency)
Pass an x-request-id header to mark a logical request. If a second request arrives with the same x-request-id while the first is in-flight, or within ~5 minutes of it completing, no new post is created. Instead you get HTTP 200 with the original post in the existingPost field.
curl -X POST "https://zernio.com/api/v1/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "x-request-id: 4b4986f4-77b5-4c22-a3a7-2c56f7a657e1" \
-d '{
"content": "Hello world!",
"platforms": [{ "platform": "twitter", "accountId": "acc_xyz789" }],
"publishNow": true
}'
# Retrying with the SAME x-request-id returns 200 with the
# original post in "existingPost" instead of double-posting.import { randomUUID } from 'crypto';
const { data } = await zernio.posts.createPost({
headers: { 'x-request-id': randomUUID() },
body: {
content: 'Hello world!',
platforms: [{ platform: 'twitter', accountId: 'acc_xyz789' }],
publishNow: true,
},
});Rules of thumb:
- Generate a fresh UUID per logical call. UUIDv4 is fine.
- Reuse the same UUID only when retrying that exact call (timeout, 5xx, connection reset).
- Or simply omit the header. Every request is then treated as new, and only Layer 2 protects you.
The generated SDKs expose the header as an explicit parameter: .XRequestId("<uuid>") in Go, x_request_id: in Ruby, a UUID argument in Java, Guid? in .NET, and x_request_id in PHP and Rust.
Workflow-tool pitfall. If your tool uses a single execution-level request ID and reuses it across multiple HTTP nodes (one ID for the whole run, shared across 6 platform calls), every call after the first looks like a retry of the first and returns its post. Generate a fresh ID per node, or omit the header.
Layer 2: Content-hash dedup (24-hour window)
Independently of x-request-id, Zernio hashes (platform, accountId, content + media URLs) and rejects duplicates within 24 hours with HTTP 409:
{
"error": "Duplicate post detected...",
"accountId": "acc_xyz789",
"platform": "twitter",
"existingPostId": "665f1c2e8b3a4d0012345678"
}This catches genuine "same content posted twice to the same account" cases regardless of headers. The response includes existingPostId so you can look up the original.
To intentionally re-post identical content within 24 hours, change something in the fingerprint: the caption, the media, or the account.
How the layers interact
Order of evaluation on each request:
- Same
x-request-idseen in the last ~5 minutes? Return the original post with HTTP 200. Nothing new is created. - Otherwise, content fingerprint seen in the last 24 hours? Reject with HTTP 409 and
existingPostId. - Otherwise, create the post.
Retry recipe
For a robust integration:
1. Generate a UUID per logical post
2. POST /v1/posts with x-request-id: <uuid>
3. On network error / timeout / 5xx: retry with the SAME uuid
4. On 200 with existingPost: treat as success (it was a replay)
5. On 409: the content already went out in the last 24h,
look up existingPostId and treat as success or surface to the userRate-limit responses (429) are also safe to retry after backing off; see rate limits.
Idempotency applies to post creation. Other write endpoints are naturally idempotent (PUT-style updates) or cheap to check before calling. When in doubt, read before you write.
Timezones & Scheduling
How scheduledFor and the timezone field interact, how queue slots handle timezones and DST, and the pitfalls that make posts publish at the wrong hour.
Post Lifecycle
Every post status, how posts move between them, what you can do in each state, and which webhook fires on each transition.