Idempotency
Making retries safe — the Idempotency-Key header and natural deduplication.
Two different mechanisms keep retries from causing damage. They're easy to confuse, so here's which applies where.
1. Natural deduplication on lead creation
POST /api/v1/leads needs no header. A lead is unique per owning agent on phone and email, so pushing the same contact twice returns the existing lead:
{ "created": false, "lead": { "object": "lead", "id": "3f1c2e00-…" } }201withcreated: true— a new lead was inserted.200withcreated: false— an existing lead matched; nothing was modified, except that a suppliedexternal_refis stamped if the lead didn't have one.
This makes a full re-sync safe: replaying your entire contact list creates only what's missing. It also means you can't use the endpoint to overwrite a lead — use PATCH for that.
2. Idempotency-Key on sends
POST /api/v1/leads/{leadId}/messages reaches a real person, so a retry after a network timeout must not double-message them. Send a unique key with each logical message:
curl -X POST https://replyfirst.ae/api/v1/leads/3f1c2e00-…/messages \
-H "Authorization: Bearer $RF_ORG_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: crm-reply-8823" \
-d '{ "text": "Free for a viewing tomorrow at 4pm?" }'Reuse the same key when retrying the same message. The second call returns 200 with deduplicated: true and sends nothing:
{
"object": "message_send",
"lead_id": "3f1c2e00-…",
"conversation_id": "8c2d41aa-…",
"channel": "whatsapp",
"deduplicated": true
}Keys are up to 255 characters. Scope them to your own system — a database row id or a UUID per outgoing message works well. A key that's too long is ignored rather than rejected, so the send falls back to the behaviour below.
Generate a new key for a genuinely new message. Reusing a key means "this is the same message I already tried to send", and the second message will not go out.
Without a key
If you omit Idempotency-Key, an identical message to the same lead within a 5-minute window is still deduplicated. That covers double-submits and fast retries, while letting you deliberately send "ok" twice an hour apart. It is a safety net, not a substitute for a key.
What is not idempotent
PATCH /api/v1/leads/{leadId} and the handoff actions are naturally idempotent — they set state rather than appending to it. Applying { "ai_paused": true } twice leaves the lead paused; accepting an already-accepted handoff leaves it accepted. No key needed.