A guest check-in system that texts a confirmation, an OTP, or a statutory notice has one job that actually matters: the message has to arrive. Everything else about the feature is decoration around that one requirement, and it is the part most integrations get wrong by treating WhatsApp as if it were guaranteed delivery instead of one channel with its own failure modes.
This is from building the notification path for a live hotel check-in platform, where a guest's confirmation and identity-verification prompts have to land reliably enough that a receptionist does not become the fallback channel.
WhatsApp fails in ways that look like success
The Cloud API returns a message id the moment a send is accepted, which is not the same thing as delivered, and delivered is not the same thing as read. A number that has never messaged the business account, a template outside its approved category, a recipient who has WhatsApp installed but no signal, a per-number rate limit reached mid-shift — all of these produce a send that looks fine in your logs and never reaches anyone.
None of that is a WhatsApp defect. It is the normal behaviour of a channel with delivery conditions outside your control, and a system that only ever tries once is betting the guest's booking confirmation on all of those conditions holding.
Designing for "it has to arrive", not "we sent it"
The shape that holds up: a message is a job in a queue, not a fire-and-forget call. It goes out on the primary channel, and the system waits on the provider's delivery webhook rather than assuming success from the send response. If a delivery receipt has not come back inside a defined window, the same message goes out on a second channel — SMS, which fails differently and mostly independently of WhatsApp's failure modes.
Two things make this safe rather than annoying:
- Idempotency. Every outbound message carries an id tied to the event it represents, not the attempt. A slow delivery receipt that arrives after the SMS fallback has already fired must not trigger a second send — the job checks its own status before acting, every time.
- A visible failure state, not a silent one. When both channels fail, that has to surface to a person — a flagged booking, a dashboard row, something a receptionist sees — rather than living only in a log nobody is watching. The worst outcome is not "the message failed", it is "the message failed and nobody found out until the guest complained".
Template approval is a deployment dependency, not a content detail
WhatsApp business messages outside a 24-hour customer-service window have to use a pre-approved template, and approval is a review process with its own latency and its own rejections — usually over wording that reads as promotional rather than transactional. Treat template approval as part of the release plan for any new notification, not something to sort out after the feature is built. A feature that is code-complete but has no approved template is not shippable, and finding that out during a release is avoidable.
Rate limits are a capacity plan, not an edge case
Messaging tiers are sized in messages per day and scale with usage history, not with your traffic on a specific afternoon. A busy check-in window can burst past a limit that looked generous in testing. The queue that handles delivery retries is the same queue that has to throttle sends against the current tier — which means the rate limit has to be a known number in the system, not a surprise returned as an error from the provider.
What this actually buys you
None of this is exotic engineering. It is the same discipline as any integration against a system you do not control: treat the send as unreliable, make it replayable, and give failure somewhere to go other than silence. The difference between a notification feature that works and one that quietly stops working for a fraction of guests every week is almost entirely in this plumbing, not in which API you called first.