
I write technical articles that analyze real world engineering problems through the lens of production systems.
My work focuses on architectural tradeoffs, UI layer system design, performance optimization, and reliability patterns derived from shipping and maintaining scalable applications.
The feature request read like it would take a day: "Let HR teams share candidate profiles and resumes directly to hiring managers on WhatsApp."
What emerged was a queue-backed, webhook-driven, delivery-tracked messaging system with failure isolation, group management, and a document persistence model I hadn't expected to care about at all. This is that story.
The first prototype was obvious: generate a temporary token, store it in the database with an expiry, and construct a WhatsApp deep link that opens a pre-filled message.
https://wa.me/?text=Check this candidate: https://yourapp.com/share/abc123
Elegant. Zero external dependencies. Ships in a day.
I tested it. It worked. And then I watched it break the product requirement entirely.
The deep link didn't open a contact picker or a structured dialog. It opened the sender's full WhatsApp window, every conversation, every contact, every group, with the pre-filled text sitting in the input box of whatever chat they had last opened. For any system dealing with sensitive HR data and candidate PII, that's not a UX problem. That's a security and compliance failure.
There was a subtler problem too, one that only becomes visible when you think about what happens on the recipient's end.
When you share a link over WhatsApp, you're sharing a URL. The recipient opens it in a browser. That browser needs to authenticate, or the document is publicly exposed to anyone with the link. One path leaks data. The other makes the link useless to unauthorized recipients. You can engineer around this with short-lived signed URLs, but now your link has an expiry.
This is actually a core architectural divergence between how WhatsApp deep links and the WhatsApp Business API handle documents, and it's worth understanding precisely because it changes your entire storage and delivery model.
Think about the last time a bank or an e-commerce platform sent you a receipt or a statement on WhatsApp. You open the chat weeks later, and the PDF is still there. It hasn't expired. You didn't need to authenticate anywhere. The document just... persists.
This is not accidental. It reflects how the Business API handles media at the protocol level.
When you send a document via the WhatsApp Business API, you first upload the file to Meta's media servers. Meta returns a media_id. You then send a message referencing that media_id. WhatsApp downloads and hosts the document on its own infrastructure. From that point forward, the document's availability is decoupled from your system entirely.
This is the fundamental tradeoff: with a deep link, you own the document URL, which means you also own its availability, authentication, and expiry. The moment the token expires or you revoke it, the link breaks. With the Business API, Meta's servers own the document after upload. Delivery becomes durable by design.
This is exactly why regulated industries like banking, insurance, and healthcare use the Business API for sending sensitive documents. Not primarily for the branding or the delivery receipts, but for this simple reason: the document's persistence is not a function of your infrastructure's uptime or your token's TTL.
With deep links eliminated, the problem reframed itself. This was no longer about generating a shareable URL. It was about building a controlled, auditable messaging pipeline that could:
Send documents to specific individuals inside a defined group
Track delivery at the message level (sent, delivered, read, failed)
Retry failed sends without human intervention
Accept status updates asynchronously via webhooks
Isolate failures per recipient, not per batch
The HR use case made this concrete. A hiring manager uploads a candidate. They want to notify a panel of four interviewers. If one interviewer's phone number is invalid, the other three should still receive the message. Failure should not be contagious.
Here is how the system was architected.
The most important design decision was introducing a group abstraction between HR users and phone numbers.
Rather than letting senders specify individual phone numbers at send time, which introduces human error, no audit trail, and no centralized control, the system maintains logical groups of recipients. An HR manager selects a group ("Senior Panel," "Founding Team," "External Recruiters") and the system resolves that to its current set of active members.
This has a number of downstream benefits that aren't obvious until you think through operational scenarios:
Membership changes don't require UI changes. When a new interviewer joins the panel, an admin adds them to the group. All future shares automatically include them. No one needs to update a contact list in a form.
Revocation is group-scoped. If a group member leaves the organization, deactivating them in the group immediately removes them from future sends. Historical logs still retain their record.
Audit is clean. Every send is logged against a group and a specific member within that group, with timestamps for each status transition. You can reconstruct exactly who was notified about a candidate, when, and whether they received it.
Sending to a group of five people looks trivially parallelizable. It isn't, for a few reasons.
Meta's Business API has rate limits. Bursting requests synchronously during an HTTP request handler risks 429 responses that you then have to handle inline, adding latency to what should be a fast user action. More importantly, network failures and API errors should trigger retries with backoff, not immediate failure surfaces to the user.
The right model is: the user action enqueues a job. The HTTP response returns immediately. A background worker processes the queue asynchronously, handles each recipient independently, and records outcomes.
Each recipient message gets its own log entry, with its own status lifecycle. If three messages succeed and one fails due to an invalid number, the job still transitions to DONE and the failed message carries its own error state. The failure is isolated.
The retry logic deserves mention. Simple retry-on-failure is easy to implement and easy to get wrong. Without backoff, a worker hammering a temporarily unavailable API endpoint creates more load at exactly the wrong moment. The retry count column in the queue table drives an exponential delay calculation: first retry after 30 seconds, second after 2 minutes, third after 10 minutes. After the configured maximum, the record is marked FAILED and surfaced to the HR administrator for manual review.
Sending a message is half the problem. Knowing whether it was delivered and read is the other half.
The WhatsApp Business API is push-based for status updates. After sending a message, Meta calls your registered webhook URL with status change events: sent, delivered, read, failed. Each event carries the meta_message_id that you stored when you sent the message.
This creates a clean two-phase model:
Phase 1 (outbound): Your worker sends the message, receives the meta_message_id, and stores it alongside the log entry with status SENT.
Phase 2 (inbound): Meta calls your webhook when status changes. You look up the log entry by meta_message_id and update the status and timestamp accordingly.
The webhook handler needs to be idempotent. Meta can deliver the same event more than once. Updating a record to the same status it already has is harmless; inserting a duplicate is not. Always upsert or check-before-update in your webhook logic.
One operational detail worth flagging: webhook endpoints must respond with HTTP 200 within a few seconds or Meta will retry delivery. Any non-trivial processing, such as database writes or downstream notifications, should happen synchronously but fast, or be handed off to another queue. Webhook handlers that time out create cascading retry storms.
The resume files live in S3, served via CloudFront. Before sending via the Business API, the worker needs to:
Fetch the binary content of the resume from S3
Upload it to Meta's media endpoint
Use the returned media_id in the message payload
The fetch step uses a short-lived S3 presigned URL generated by the worker at runtime. This URL is never exposed to the client or stored anywhere. It exists only long enough for the worker to pull the bytes and upload them to Meta. Once Meta has the file, the presigned URL can expire and the document's availability is entirely Meta's responsibility.
This is a clean separation: S3 holds the canonical, access-controlled copy. Meta holds the WhatsApp-deliverable copy. Neither leaks to unauthenticated users, and neither's availability depends on the other.
The system was built and validated against Meta's developer test environment, which provides a free test number valid for 90 days. This is not a staging environment in the traditional sense. It's a real API surface with real rate limits, real webhook behavior, and real template approval requirements.
Template approval was the first friction point, and understanding why it exists changes how you think about the entire platform.
WhatsApp is fundamentally a consent-based messaging channel. Unlike email, where anyone can send anything to any address, WhatsApp distinguishes sharply between two conversation types. A user-initiated conversation, where a person messages a business first, opens a 24-hour window during which the business can reply freely. A business-initiated conversation, where the business reaches out to a user who has not messaged them, is only permitted using pre-approved message templates.
This distinction exists to prevent spam at platform scale. Meta reviews every template before it can be used to initiate a conversation. The review covers message structure, variable usage, and the category of content being sent. Templates that could be used for promotional spam, misleading claims, or unverifiable content are rejected. Approved templates are locked. You cannot modify the body text after approval; you can only populate the defined variable placeholders at send time.
For our candidate sharing use case, this meant the message structure had to be decided and approved before a single message could be sent. The template carried structured candidate details as numbered variables alongside a document component for the resume. Each variable maps to a specific piece of candidate data populated by the worker at send time. Getting this approved required submitting the template through Meta's Business Manager, waiting for review, and then referencing the approved template name and language code in every API call.
This is not a developer inconvenience. It is a deliberate platform constraint that has real implications for your delivery timeline. Unlike most API integrations where you can ship and iterate freely, the WhatsApp Business API requires you to design your message structure upfront and get it approved before you can test with real numbers outside the developer sandbox. Any structural change to an approved template requires submitting a new template for review. Plan for this lead time explicitly.
The 90-day prototype window made one thing clear: the Business API is enterprise infrastructure, not a developer utility. The approval flows, the tiered access model, and the conversation-based billing are all designed for organizations sending at scale, with compliance teams and dedicated integration engineers. Building a prototype is accessible. Productionizing it requires organizational alignment that goes beyond the engineering work.
Although the implementation remained at prototype stage due to shifting product priorities, the architecture and integration were validated end-to-end against Meta's developer environment. The queue processed jobs correctly, webhooks updated delivery status in real time, group resolution handled member deactivation cleanly, and failure isolation behaved exactly as designed. The engineering outcomes were complete.
This is a normal part of product development. Priorities shift, roadmaps change, and not every validated prototype reaches production. What matters is that the architectural decisions were sound and the tradeoffs were understood.
The most durable insight from this work was the document persistence model. The hard part turned out not to be the API integration. It was understanding why the Business API exists for this use case in the first place. Uploading to Meta's media servers transfers durability responsibility entirely away from your infrastructure. When an insurance company sends you a policy document on WhatsApp, they are not managing token expiry or authentication. They have offloaded that concern to a platform built to handle it at scale.
That understanding shapes how you evaluate every future decision involving document sharing, regardless of the delivery channel.
Group abstraction over direct phone numbers - Centralizes membership management, enables clean revocation, produces auditable send history without requiring senders to manage contact lists.
Queue-first, synchronous-never - User actions enqueue work and return immediately. Background workers handle API calls, retries, and status updates. The user experience is decoupled from the reliability of an external API.
Per-recipient failure isolation - A failed send to one group member does not fail the batch. Each message log entry carries its own status and error state independently.
Webhook-driven status - Delivery and read receipts arrive asynchronously via Meta's webhook infrastructure. The message log is the single source of truth for status, updated by both the outbound worker and the inbound webhook handler.
Ephemeral presigned URLs for media transfer - The worker generates short-lived S3 URLs at send time to fetch resume bytes. These are never stored or exposed. After upload to Meta, document availability is Meta's responsibility.
Deep link vs. Business API - Deep links are free and zero-dependency, but expose the sender's full contact list and produce links that expire. The Business API requires template approval and conversation-based billing, but delivers durable documents that persist in chat and never require recipients to authenticate anywhere.
The right choice between them is not a technical question. It's a product question about who your recipients are, what your compliance requirements look like, and whether your organization is willing to operate inside Meta's approval and billing model. For an internal HR platform sharing candidate documents with a controlled list of recipients, the Business API was the only path that satisfied the actual requirements.
Although the feature was not ultimately released due to shifting product priorities, the implementation was fully built, integrated, and validated end-to-end against Meta’s WhatsApp Business API developer environment. The system handled queue-backed delivery orchestration, webhook-driven status tracking, retry workflows, and group-scoped failure isolation as designed.