Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Prevent Duplicate iMessages After a Timeout

A network timeout tells you one thing: you stopped waiting. It says nothing about whether the bridge accepted your request. By the time your client dropped the connection, the Mac worker may already have handed your message to Apple's iMessage infrastructure. Issue a second POST with fresh parameters and the bridge has no basis to link it to the first — it enqueues a second delivery.

The fix is structural: attach a stable Idempotency-Key header to every send, then poll the receipt endpoint after any uncertain outcome before you consider retrying.

Two scopes, one rule about chatGuid

The messages:send scope is separate from messages:read and must be explicitly requested during the OAuth authorization step. It grants access only to selected existing conversations — a conversation that appears in a read result is not automatically authorized for sends; your send grant must include that specific thread.

Every chatGuid value used in a send or read request must come from the chat list API. Do not construct or guess these values manually.

Generate and persist the key before the first request

Create one key per intended send and freeze the exact request payload — both before the first HTTP call — and write them to your own durable store immediately. If your process crashes mid-flight, you need that key to poll the receipt later. A pending action carries no guaranteed server-side retention; your store is the source of truth.

Do not compute the key from a hash of the message body. Two separately intended sends of identical text would share a hash and merge into a single delivery. Encode intent with a unique component:

{appId}-{connectionId}-{conversationRef}-{uuid}

For example: app_x9a-con_abc123-thread_home-01JDF3. Never generate a new key when retrying the same intent. A new key signals a new intent; the bridge treats it as a fresh request, risking a second delivery — failures or validation can stop either request.

Send with the Idempotency-Key header

POST https://figment.so/api/apple-connect/v1/connections/con_abc123/messages/send
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
Idempotency-Key: app_x9a-con_abc123-thread_home-01JDF3

{
  "chatGuid": "CHAT_GUID_FROM_LIST_API",
  "text": "Your order shipped."
}

The bridge binds the key to this connection and the exact request body. A subsequent POST with the same key and unchanged body returns the original recorded outcome without re-delivering — the response carries "replayed": true. If the body changes while the key stays the same, the request is rejected — a changed payload during retry must stop, not silently mint a new key. Use a new key only for an explicitly approved new action, not to handle an uncertain retry.

One example of a first-send response in live mode:

{
  "data": {
    "idempotencyKey": "app_x9a-con_abc123-thread_home-01JDF3",
    "state": "submitted",
    "retrySend": false
  },
  "mode": "live",
  "replayed": false
}

state: submitted means the Mac accepted the message; it is not proof of delivery to the recipient's device. In sandbox mode, the state is simulated and no real message is sent:

{
  "data": {
    "idempotencyKey": "app_x9a-con_abc123-thread_home-01JDF3",
    "state": "simulated",
    "delivery": "simulated",
    "retrySend": false
  },
  "mode": "sandbox",
  "replayed": false
}

Poll the receipt before retrying

After a timeout or an unknown response, do not issue a new send. Poll the receipt endpoint with the same Idempotency-Key:

GET https://figment.so/api/apple-connect/v1/connections/con_abc123/messages/receipt
Authorization: Bearer ACCESS_TOKEN
Idempotency-Key: app_x9a-con_abc123-thread_home-01JDF3
StateMeaningYour next action
submittedMac is processingRe-poll; treat as in-flight
deliveredDelivered to recipient's deviceNo retry needed
readRecipient opened the messageNo retry needed
failedSend action recorded as failedSame key returns the recorded failure, not a new attempt; decide independently whether to start a new action under a new key
unknownOutcome indeterminateRe-poll after a brief wait; never resend under a new key
simulatedSandbox synthetic receiptNo real delivery occurred

A client-side timeout with no response is functionally unknown; treat it as such and re-poll with the original key.

Idempotency and receipt reconciliation test cases

The seven scenarios below cover the edge cases that can produce duplicates or unexpected behavior:

#ScenarioSame key on retry?Body changed?Expected result
1First send; clean response——Live: current receipt state (e.g. submitted); sandbox: simulated; replayed: false
2Client timeout; retry with identical requestYesNoSame recorded outcome; replayed: true
3Retry with edited text; same keyYesYesRequest rejected — fix body or generate a new key only for an explicitly approved new action
4unknown; receipt transitions to delivered——No retry needed
5unknown; receipt remains unknown after sync settles——Re-poll; check Mac connection readiness before escalating
6Two workers race to retry with the same key and bodyYesNoConcurrent-retry behavior is not independently verified; treat this as an integration test requirement before relying on it in production
7New distinct message after confirmed deliveryNo — new keyNew bodyNew delivery; generate a fresh key before the first attempt

Scenario 6 is relevant when retry logic runs across distributed workers. Verify concurrent-retry behavior in your own integration tests; do not assume a specific outcome before testing.

SMS, MMS, and RCS threads

The same idempotency pattern applies to SMS, MMS, and RCS threads. Messages in iCloud includes forwarding when enabled; the iPhone must be powered on, online, and signed into the same Apple Account (Apple: Use Text Message Forwarding). An unknown state on a carrier thread may reflect an iPhone connectivity issue rather than a bridge failure; unknown remains unknown until receipt evidence resolves it.


Ready to send iMessages from your app without duplicates? Connect Apple Account to link a Mac and start building. Access starts after setup; sandbox is free, and the developer subscription is $29/month per active live connection.


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms