Products

Ships to

Developer guideiMessage API6 min read

Idempotency keys for messaging APIs

How Flow idempotency keys let you retry a send without messaging someone twice: the rules, good keys for agents, what a key cannot cover and how to handle timeouts.

The short answer

Flow makes a send safe to retry: an idempotency key returns the first answer and does not send twice. Use the inbound event's ID as the key when you reply, keep it the same across retries and know the one case it cannot cover.

Building on the iMessage API? Build on iMessage with Flow: one agent for iMessage, WhatsApp and Telegram. New to it? Read the iMessage API guide.

The problem

Networks fail in the middle. You send a message, the connection drops, and you do not know whether it went out. If you retry, the customer may get two messages. If you do not, they may get none. For an agent that confirms bookings or orders, either is a bad day.

An idempotency key solves the first half. Send iMessage programmatically shows where it fits in the send call.

How an idempotency key works

You send an Idempotency-Key header with a POST. A repeat with the same key returns the first answer and does not act twice. This is from Flow’s idempotency docs:

curl https://api.flow.engineer/v1/conversations/conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1/messages \
  -H "Authorization: Bearer $FLOW_MESSAGING_KEY" \
  -H "Idempotency-Key: evt_01JB8ZC3K5M7P9R1T3V5X7Z9B1" \
  -H "Content-Type: application/json" \
  -d '{"content": {"type": "text", "text": "Got it!"}}'

The rules:

  • Any unique string up to 255 characters works. A UUID or ULID is a good choice.
  • Keys are kept for 24 hours per app and mode.
  • A repeat returns the first answer with the header Idempotent-Replayed: true.
  • Reusing a key with a different method, path or body fails with 409 idempotency_conflict. So does a repeat that arrives while the first request is still running.
  • Answers worth retrying (429, 500, 502, 503, 504) are not kept, so a retry with the same key runs again.

Good keys for agents

The key should come from something that is the same on every retry:

  • Replying to an event: use the event’s id, plus the bubble index when you send several messages (evt_...:0, evt_...:1). A redelivered webhook then cannot make your agent answer twice.
  • Webhook replies already use the event’s id as their key.
  • Stream frames: the ref of a send frame is its idempotency key.
  • The TypeScript SDK generates a key for every POST and reuses it across its own retries. Pass idempotencyKey to choose it yourself.

A random key per attempt defeats the purpose. If your code generates a new UUID on each retry, the API sees a new request every time.

In TypeScript:

await event.conversation.reply("You said: " + said, { idempotencyKey: event.id });

The Node.js guide and Python guide show the same in context.

What a key does not cover

The key protects your requests. It does not protect the hop to the channel. When a channel takes a send but never answers, Flow cannot tell whether it arrived, so it sends it once more. On Telegram, which has no way to check, the person can rarely get it twice. On the other channels, Flow first waits for the channel’s own report.

If the outcome is still unknown, the message fails with channel_timeout_unknown_outcome. Check the conversation before you send it again.

Also remember that a key protects one request, not one intent. If your agent decides twice to confirm the same booking, it needs its own guard, such as checking the booking’s state before it acts.

Retry rules

  • Retry what clears by itself. rate_limited and sender_throttled carry retry_after. Wait, then retry with the same key.
  • Do not retry what will not clear. outside_window stays until the person writes again.
  • Keep the body identical. A different body with the same key is a conflict.
  • Mind the 24 hours. After that a key is gone, so do not rely on one for a retry a day later.
  • Check before a manual resend after an unknown outcome.

For limits and sending rules, see iMessage API rate limits and sending rules. For retries on the receiving side, see iMessage webhooks.

Frequently asked questions

What is an idempotency key?

A unique string you send with a POST. If you repeat the request with the same key, the API returns the first answer and does not act twice, so a retry never sends a second message.

How long are keys kept?

On Flow, 24 hours per app and mode. A key can be any unique string up to 255 characters, and a UUID or ULID is a good choice.

What key should an AI agent use?

Use the inbound event's ID when you reply to it, plus the bubble index when you send several messages, such as evt_123:0 and evt_123:1. A redelivered webhook then cannot make your agent answer twice.

What happens if I reuse a key for a different request?

It fails with 409 idempotency_conflict. So does a repeat that arrives while the first request is still running.

Are failed requests kept?

Answers worth retrying, such as 429, 500, 502, 503 and 504, are not kept, so a retry with the same key runs again.

What can an idempotency key not protect against?

The hop to the channel. If a channel takes a send but never answers, Flow cannot tell whether it arrived, so the outcome can be unknown, and you should check the conversation before you send again.

Start hereiMessage API: a developer’s guideThe overview of how iMessage APIs work, and what to ask a provider. Developer guideSend iMessage programmaticallycurl, TypeScript and Python: get a key, receive a signed webhook, reply once and handle retries.Developer guideiMessage API without a MacWhy a hosted iMessage API removes the Mac, what the DIY relay costs you, and how to choose.Developer guideiMessage MCP serverTwo things people mean by it: a messaging MCP for your coding agent, and your product as an MCP server.Developer guideiMessage API for Node.js and TypeScriptInstall the SDK, stream inbound messages, verify signed webhooks in Express and reply safely.Developer guideiMessage API for PythonStream inbound events, verify signed webhooks in FastAPI and reply with idempotency keys.Developer guideiMessage API for GoPoll events, reply and verify signed webhooks with the standard library, no SDK needed.

All posts: the Flow blog. The Flow docs have the reference.

From reading to building

Your agent, on iMessage and everywhere else.

Flow gives your agent an iMessage line, and the same code answers on WhatsApp and Telegram and works as an MCP server and CLI. See how it works on iMessage.

Build on iMessage with Flow