What counts as a failure
A delivery succeeds only when your endpoint returns a2xx within 10 seconds. Everything else is a failure and gets retried:
- any
4xxor5xx, including a401from your own signature check; - any
3xx: redirects are not followed, so register the final URL; - no response within 10 seconds, or a connection that can’t be made (shown as code
0in the dashboard).
Retry schedule
After the 5th failed attempt the delivery is marked dead and never retried. From the first attempt to the last is about 37 minutes.
iBubbl sends deliveries in batches every 30 seconds, so each attempt can arrive up to 30 seconds after the time above, and a burst of events can add more delay. Don’t build logic on exact timings.
Every attempt carries the same body and the same event
id, but a new signature timestamp: each attempt is signed when it is sent, so a retry always passes the 5-minute replay check.
What iBubbl does not do
- No auto-disable. However many deliveries fail, your subscription stays active, and new events keep trying. Only you can turn it off, by revoking it.
- No replay. A dead delivery isn’t resent, and there’s no endpoint to resend it. To catch up after an outage, list what you missed with
GET /v1/messages: see Recover from downtime. - No delivery to revoked or later subscriptions. Revoking a subscription stops its pending retries. A subscription you create after an event doesn’t receive that event.
Watching deliveries
The dashboard’s Settings → Webhooks → Recent deliveries shows every attempt: status (Delivered, Pending or Dead), event type, your response code, the attempt number out of 5, and the time. Delivery records are kept for 30 days. Test webhooks locally explains each column.Return 2xx fast
The 10-second timeout is firm. A slow handler fails even when it eventually succeeds, and then gets the same event again. Verify the signature, record the event, return 2xx, and do the real work afterwards in a queue or background task.
At least once: de-duplicate
The same event can reach you more than once:- your endpoint processed it but the response was lost, timed out or wasn’t
2xx; - you have two subscriptions that both match the event type: each gets its own delivery, with the same event
id; - in rare cases iBubbl records the same change twice as two events (for example, the
message.sentfor a new group’s first message).
id (evt_…), which is identical on every retry and across subscriptions. For the rare double-recorded change, also de-duplicate on data.message_id plus type. Don’t de-duplicate on body content: different events can have identical payloads.
2xx for a duplicate too: an error would only make iBubbl retry it again.
The
message_key check drops a second event of the same type for the same message. That’s right for every message.* status event. For message.edited, a contact can edit one message up to five times, each a real, separate event, so leave message_key empty for edits. Reaction events carry reaction_id rather than message_id, so the event id alone covers them.Out of order: don’t trust arrival order
Events can arrive in a different order than they happened: a retry of an older event can land after a newer one, and events in the same batch aren’t sent in a fixed order. For example,message.read can arrive before message.delivered, and contact.opted_out before or after the message.inbound that triggered it.
- Order events by the envelope’s
created_at, not by when they arrived. - Don’t let a late event move a message backwards in your store: once you’ve recorded
read, ignore a later-arrivingdelivered. A message can genuinely move fromsenttofailed, though; see Statuses that move backwards. - When in doubt,
GET /v1/messages/{id}returns the current state.
Recover from downtime
After about 37 minutes of failures a delivery is dead, and iBubbl never resends it. If your endpoint was down longer than that, or you shipped a handler with a bug, webhooks alone won’t tell you what you missed. List messages will: it returns every message, inbound and outbound, with its current status. The recipe: keep a checkpoint, the newestcreated_at you’ve processed. To catch up, list everything created since a little before the checkpoint, handle what you haven’t seen, update what changed, then move the checkpoint forward.
- Start a few minutes early. Messages arriving at the same moment can become visible in either order, so one with a slightly older
created_atcan show up after you’ve read a newer one. Starting 5 minutes before the checkpoint covers that; the overlap is harmless because of step 2. - De-duplicate by message. A message can reach you here and also as a late webhook retry. The samples pass each inbound message through the same
isFirstDeliverycheck as the webhook handler, with the samemessage_key(<message id>:message.inbound), so whichever arrives second is skipped. - Overwrite the status. The list returns each message’s current status, so unlike a webhook it can’t be out of date: store it as it is.
- Save the checkpoint only after the last page. If a run fails part-way, the next one starts from the old checkpoint, and step 2 makes the repeat safe.
reconcile: prefix keeps the made-up event id apart from real evt_… IDs; the message_key is what matches a webhook for the same message.
Run it when your endpoint comes back from an outage, or on a schedule (every 15 minutes, say) as a safety net. created_at values are fixed-width UTC strings ending in Z, so comparing them as text, as the samples do, gives the right order.
Two limits to know:
- It finds messages created since the checkpoint. A message created earlier whose status changed during the outage (yesterday’s
sentthat becamedelivered) isn’t in that window. If you need those, look back further withcreated_after, or check the messages you still hold asqueued,pendingorsentwithGET /v1/messages/{id}. - Missed inbound messages can be hours old. Before an automated reply to one, check its
created_at: an answer hours after the contact wrote can be worse than none.
Next
Receive messages
A complete receiver, with de-duplication and a safe auto-reply.
Webhook events
Every event type and its payload.
Pagination
The
listMessages loop the recovery recipe uses.
