# Idempotency and safe retries

Send an Idempotency-Key so a retried create returns the original result instead of a second post, message, call or ad.

Networks drop responses. A job runner retries a step, a no-code tool fires a node twice, a user double-clicks. Without protection, each of those can publish the same post twice or send the same SMS twice. The `Idempotency-Key` header stops that: Pozy remembers the result of the first request with a given key and hands it back for any repeat.

## Send a key on creates

Generate one key per logical action, store it next to the job, and send the same key on every attempt.

```bash cURL
curl -X POST https://api.pozy.ai/v1/posts \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0b6f1c2e-7a43-4d8e-9f15-3c2a8e4b7d91" \
  -d '{
    "content": "Doors open at 9.",
    "platforms": [{ "platform": "linkedin", "accountId": "6720f41a8d3be96c1e5a02f7" }],
    "scheduledFor": "2026-11-05T09:00:00",
    "timezone": "Europe/London"
  }'
```
```ts Node
import { randomUUID } from "node:crypto";
const key = job.idempotencyKey ?? (job.idempotencyKey = randomUUID()); // persist with the job

const res = await fetch("https://api.pozy.ai/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Idempotency-Key": key,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    content: "Doors open at 9.",
    platforms: [{ platform: "linkedin", accountId: "6720f41a8d3be96c1e5a02f7" }],
    scheduledFor: "2026-11-05T09:00:00",
    timezone: "Europe/London",
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
```
```python Python
import os, uuid, requests

key = job.get("idempotency_key") or str(uuid.uuid4())  # persist with the job
resp = requests.post(
    "https://api.pozy.ai/v1/posts",
    headers={
        "Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
        "Idempotency-Key": key,
    },
    json={
        "content": "Doors open at 9.",
        "platforms": [{"platform": "linkedin", "accountId": "6720f41a8d3be96c1e5a02f7"}],
        "scheduledFor": "2026-11-05T09:00:00",
        "timezone": "Europe/London",
    },
    timeout=30,
)
```

Rules for keys:

- Any string up to 255 characters. A UUID v4 is the simplest choice. A longer key returns `400`.
- One key per action. Reusing a key for a different post returns the first post, not a new one.
- Keys are remembered for 24 hours and are scoped to your workspace.
- Generate the key before the first attempt, not inside the retry loop.

## What a repeat returns

For posts, the first request creates the post and returns `201`. A repeat with the same key within 24 hours creates nothing and returns `200` with the original post, even if the body changed in between. Treat both as success.

```json
{
  "message": "Post already exists (idempotent retry)",
  "post": { "_id": "6721a0c47b2e5f9d3a18c6e0", "status": "scheduled" }
}
```

If the first request is still being processed when the repeat arrives, you get `409` with `code: "idempotency_conflict"` and a `Retry-After` header. Wait that long and send the same request again.

For the other endpoints below, only successful responses are remembered. If the first attempt failed, the key is released and the retry runs for real. The same key sent to a different path, or with a different body, returns `422`.

## Endpoints that accept a key

| Area | Endpoints |
| --- | --- |
| Publishing | [Create post](https://pozy.ai/docs/api/createPost), [create blog article](https://pozy.ai/docs/api/createBlogArticle) |
| Inbox | [Send message](https://pozy.ai/docs/api/sendInboxMessage), [reply to comments](https://pozy.ai/docs/api/replyToInboxPost), [reply to a review](https://pozy.ai/docs/api/replyToInboxReview) |
| Phone and messaging | [Send SMS](https://pozy.ai/docs/api/sendSms), [send RCS message](https://pozy.ai/docs/api/sendRcsMessage), [create RCS agent](https://pozy.ai/docs/api/createRcsAgent), [start a voice call](https://pozy.ai/docs/api/createVoiceCall), [start a WhatsApp call](https://pozy.ai/docs/api/initiateWhatsAppCall) |
| Branded calling | [Create enterprise](https://pozy.ai/docs/api/createBrandedCallingEnterprise), [create identity](https://pozy.ai/docs/api/createBrandedCallingIdentity) |
| Ads | [Create campaign](https://pozy.ai/docs/api/createAdCampaign), [create ad set](https://pozy.ai/docs/api/createAdSet), [create ad](https://pozy.ai/docs/api/createStandaloneAd), [boost a post](https://pozy.ai/docs/api/boostPost), [messaging ad](https://pozy.ai/docs/api/createMessagingAd), [call ad](https://pozy.ai/docs/api/createCallAd), and the duplicate endpoints for campaigns, ad sets and ads |
| Creator campaigns | Create campaign, raise budget, add participant, approve, reject or reopen a submission, adjustments, payout batches |
| Money | Top-ups and transfers |
| Support | [Ask the support assistant](https://pozy.ai/docs/api/askSupportQuestion) |

The [API reference](https://pozy.ai/docs/api) lists `Idempotency-Key` under the header parameters of every endpoint that takes it. Other writes are either updates (sending them twice leaves the same result) or cheap to check first.

> **Profiles use externalId:**
> 
> To make profile creation retry-safe, set `externalId` to your own customer id. A second create with the same `externalId` returns `409` with `code: "profile_external_id_conflict"` and the existing profile's id in `details.existingProfileId`, so you can carry on with that profile ([profiles](https://pozy.ai/docs/build/profiles)).

## Duplicate content protection

Separately from keys, Pozy refuses to publish the exact same content to the same account twice within 24 hours. A match on the account, the text and the media URLs returns `409`:

```json
{
  "error": "This exact content is already scheduled, publishing, or was posted to this account within the last 24 hours.",
  "details": {
    "accountId": "6720f41a8d3be96c1e5a02f7",
    "platform": "linkedin",
    "existingPostId": "6721a0c47b2e5f9d3a18c6e0"
  }
}
```

Only scheduled, publishing and published targets count. Failed and cancelled ones do not. To repost on purpose, change the text, the media or the account.

If you send your own `x-request-id` and a retry carries the same request id and the same content, Pozy treats it as a retry and returns the original post rather than this `409`. `Idempotency-Key` is still the better tool: it works even when the body changes.

## A safe retry recipe

1. Create the key and save it with your job before the first attempt.
2. Send the request with the key.
3. On a timeout, a connection error, `429` or `5xx`: wait (honour `Retry-After`, otherwise back off 1 s, 2 s, 4 s...), then send the identical request with the same key.
4. On `200` or `201`: store the returned id. You are done.
5. On `409 idempotency_conflict`: wait `Retry-After` and go back to step 3.
6. On any other `4xx`: stop and fix the request. Retrying will not help.

> **In workflow tools:**
> 
> In n8n, Make or Zapier, create a fresh key per HTTP step. One key shared by several steps turns every step after the first into a "retry" of the first, and nothing else gets created.
