Skip to content

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

GuaranteeHow
An accepted message (202) is never lostStored in PostgreSQL before the response; the database is the queue
A retried send never creates a second messageRequired Idempotency-Key, stored in the same transaction
Every state change produces its eventTransactional 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 deploymentsEncrypted credentials in PostgreSQL; sessions are restored automatically
Two workers never hold the same WhatsApp sessionDatabase 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 UNKNOWN instead 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

  1. The API stores the message as QUEUED (with its idempotency key, status event, outbox event and usage record).
  2. The worker owning the instance claims the oldest due message (PROCESSING), assigns the WhatsApp message ID and records an in_flight attempt — all in one transaction, before calling WhatsApp.
  3. The worker sends. Outcomes:
    • success → SENT (inferred), then SENT (confirmed) when WhatsApp's server acknowledges, then DELIVERED/READ;
    • the instance was not connected → back to QUEUED, retried with exponential backoff until expiresAt, then FAILED (EXPIRED);
    • WhatsApp rejected it before sending (invalid recipient, missing media) → FAILED;
    • any other error, where transmission may have happened → UNKNOWN.
  4. If the worker crashed between 2 and 3, the next worker that takes over the instance finds the in_flight attempt and marks the message UNKNOWN (it was claimed but never attempted → simply re-queued).
  5. A receipt for the message ID upgrades UNKNOWN to SENT/DELIVERED/READ.
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/SENT as "in progress", DELIVERED/READ as success, FAILED as failure, UNKNOWN as "needs a decision".
  • If you missed webhooks (downtime), reconcile with GET /v1/events or GET /v1/messages for the period.

Connection recovery

SituationWhatsTIG does
Network dropReconnects 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 phoneLOGGED_OUT, credentials wiped, no reconnect loop
Session replaced by another clientDISCONNECTED (CONNECTION_REPLACED), does not fight for the session
Account restrictedRESTRICTED, stops
QR not scannedStops after ~3 minutes (QR_TIMEOUT)
Worker restart / deploymentLeases 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.