Retries, recovery & delivery semantics
This page explains what WhatsTIG guarantees, what it does not, and how your integration should handle each case.
What is guaranteed
| Guarantee | How |
|---|---|
An accepted message (202) is never lost | Stored in PostgreSQL before the response; the database is the queue |
| A retried send never creates a second message | Required Idempotency-Key, stored in the same transaction |
| Every state change produces its event | Transactional outbox: event and change commit together |
Webhook events are delivered at least once (or end up dead and visible) | Retries with backoff for ~16 h, delivery log, replay |
| A linked account survives restarts and deployments | Encrypted credentials in PostgreSQL; sessions are restored automatically |
| Two workers never hold the same WhatsApp session | Database leases with fencing tokens (see Scaling) |
What is not guaranteed
- Delivery. WhatsApp may delay or drop messages, the recipient may not exist, and accounts can be restricted.
- Exactly-once sending. If a worker stops in the middle of a send, the message may or may not have reached
WhatsApp. WhatsTIG marks it
UNKNOWNinstead of guessing, and never resends it automatically. - Receipts. Delivery and read receipts depend on the recipient's privacy settings.
- Ordering of webhook events.
How sends are processed
- The API stores the message as
QUEUED(with its idempotency key, status event, outbox event and usage record). - The worker owning the instance claims the oldest due message (
PROCESSING), assigns the WhatsApp message ID and records anin_flightattempt — all in one transaction, before calling WhatsApp. - The worker sends. Outcomes:
- success →
SENT(inferred), thenSENT(confirmed) when WhatsApp's server acknowledges, thenDELIVERED/READ; - the instance was not connected → back to
QUEUED, retried with exponential backoff untilexpiresAt, thenFAILED(EXPIRED); - WhatsApp rejected it before sending (invalid recipient, missing media) →
FAILED; - any other error, where transmission may have happened →
UNKNOWN.
- success →
- If the worker crashed between 2 and 3, the next worker that takes over the instance finds the
in_flightattempt and marks the messageUNKNOWN(it was claimed but never attempted → simply re-queued). - A receipt for the message ID upgrades
UNKNOWNtoSENT/DELIVERED/READ.
Recommended integration pattern
your job ──► POST /v1/messages (Idempotency-Key = your notification id)
├─ 202 → store msg id, done
├─ 4xx (except 409 IN_PROGRESS / 429) → fix data, do not retry
└─ network error / 429 / 5xx → retry later with the SAME key
webhooks ──► dedupe by event id → update your notification status (ignore regressions)
nightly ──► GET /v1/messages?status=UNKNOWN&createdAfter=... → decide per business rule
- Treat
QUEUED/SENTas "in progress",DELIVERED/READas success,FAILEDas failure,UNKNOWNas "needs a decision". - If you missed webhooks (downtime), reconcile with
GET /v1/eventsorGET /v1/messagesfor the period.
Connection recovery
| Situation | WhatsTIG does |
|---|---|
| Network drop | Reconnects with exponential backoff (2 s → 5 min, up to 12 attempts), status RECONNECTING; then ERROR |
| WhatsApp asks for a restart (normal after pairing) | Reconnects immediately |
| Device removed on the phone | LOGGED_OUT, credentials wiped, no reconnect loop |
| Session replaced by another client | DISCONNECTED (CONNECTION_REPLACED), does not fight for the session |
| Account restricted | RESTRICTED, stops |
| QR not scanned | Stops after ~3 minutes (QR_TIMEOUT) |
| Worker restart / deployment | Leases are released (graceful) or expire (crash, ~30 s); another worker restores the session from stored credentials |
Queued messages simply wait while an instance is reconnecting, and expire if it does not come back in time.