
WhatsApp Webhook Security: Signature, Replay and Rate Limits
How to secure the WhatsApp webhook behind your AI agent: verify Meta's signature on the raw body, block replayed events and apply rate limits.
Want to apply this in your company?
Book a call with Juan →WhatsApp Webhook Security: Signature, Replay and Rate Limits
WhatsApp webhook security comes down to three layers: signature, replay protection and limits. To protect the webhook that feeds an AI agent, validate the X-Hub-Signature-256 header against the raw request body using your Meta app secret. Then discard repeated events by message ID, and apply rate and size limits before any call to the model. Together these layers stop someone from forging messages, reusing legitimate requests or exhausting your operation. Order matters: authenticate, deduplicate, limit, and only then hand the event to the agent.
Key takeaways
- The signature proves the request came from Meta and was not altered, but it only works when calculated over the original bytes of the body.
- The webhook delivers events at least once, so repeats are normal behavior and deduplication by identifier is mandatory.
- Rate limits should act after validation and, ideally, at the agent's queue, so they do not trigger retries and lost messages.
- A valid signature does not make the content safe: the customer's text is still untrusted input for the model.
- Log decisions (accepted, rejected, duplicate, throttled) without storing unnecessary personal data, in line with LGPD.
Why is the webhook the most sensitive door into your agent?
Your webhook endpoint is a public URL on the internet. Anyone who finds it can send a request that looks like a WhatsApp message. If your agent trusts that body, an attacker can impersonate a customer, trigger internal workflows, run up model costs or provoke replies to numbers that never wrote to you.
In a customer service agent the risk is larger because the webhook does not end in a log table. It triggers reasoning, queries your systems and sometimes sends messages. That is why we treat the endpoint as a security boundary, not an integration detail. For the broader picture of the integration, see how to integrate AI into your company's WhatsApp.
A note on sources: what we describe about the header, the secret and the algorithm comes from Meta's official documentation. The recommendations on deduplication and limits are common engineering practice for webhooks, and we flag as a hypothesis anything that depends on your volume and infrastructure.
How do you validate Meta's signature, step by step?
Meta signs each notification with an HMAC SHA-256 of the request body, computed with your app secret, and sends the result in the X-Hub-Signature-256 header with the prefix sha256=. Your job is to recompute the value and compare.
- Read the raw request body as bytes, before any JSON parser touches it.
- Get the App Secret from a secrets vault or a protected environment variable. Do not use the verify token or the access token for this.
- Compute the HMAC SHA-256 of the bytes with that secret and convert it to hexadecimal.
- Build the expected string with the sha256= prefix and compare it to the header using a constant-time comparison.
- If the header is missing, malformed or different, reject the request with a generic error and process nothing.
- Only after validation, parse the JSON and continue the flow.
The most common mistake is validating against JSON that has already been parsed and reserialized. Changes in whitespace, key order or escaping of accented characters alter the bytes and make a legitimate signature fail. With Portuguese or Spanish content this shows up fast, because names and messages carry accents and emoji. Many frameworks consume the body before your code runs, so configure the server to keep a copy of the original bytes.
Other implementation points:
- Never write the secret to logs or send it to monitoring tools.
- Plan for secret rotation: define who can rotate it and how the service reloads the value without downtime.
- In test environments, use a separate app and secret from production.
Meta's official Graph API webhook documentation describes this mechanism and is the final reference for the header format. For a comparable pattern from another provider, Stripe's webhook documentation shows the same signature logic over the raw body.
What does the initial endpoint verification do, and what doesn't it do?
When you register the URL in the Meta dashboard, Meta sends a GET request with the parameters hub.mode, hub.verify_token and hub.challenge. Your endpoint compares the verify token with the value you set and returns the challenge it received. This confirms you control the URL.
This step does not authenticate later notifications. The verify token is a configuration secret, and real messages arrive by POST, protected by the signature. Two practical points: answer the verification GET only when the token matches, and never accept a POST without checking the signature just because the initial verification passed.
If your number still depends on the business account, review the process for Meta Business Verification for the WhatsApp API, which comes before configuring the webhook in production.
How do you protect against replay?
There are two kinds of repetition, and they call for different responses.
The first is legitimate repetition. If your endpoint is slow or returns an error, Meta tries to deliver again, so the same event can arrive more than once. The second is malicious repetition: someone captures a valid request (for example from a poorly protected log) and resends it. In both cases the signature is still correct, because the body is identical. That is why a signature alone does not prevent replay.
As far as the official documentation indicates, the notification does not carry a separate signed timestamp in a header. The practical defense is idempotency: handle each event exactly once.
- Choose the deduplication key: for incoming messages, the message ID (wamid); for status updates, combine the ID and the status.
- Write the key atomically, with a uniqueness constraint in the database or an insert-if-absent operation in a cache. Checking and then writing in two steps leaves room for a race.
- If the key already exists, respond with success and do not reprocess. Responding with an error only encourages more retries.
- Keep keys for longer than the period in which Meta may resend. The exact value depends on current documentation and your tolerance, so treat it as a parameter to review.
- Also verify that the phone number ID (phone_number_id) in the payload is one of yours. Events for another number should not enter the flow.
There is a second layer: idempotent side effects. Even with deduplication at the entry point, a failure midway through processing can lead to an internal retry. If the agent creates an appointment or sends a reply, record the intent with its own key so the action does not run twice. Stripe's documentation on idempotent requests is a good example of how a provider defines this contract, and the idea applies to your own actions.
On rejecting old events using the timestamp inside the payload: because the body is signed, the value is reliable. But discarding by age can drop legitimate late deliveries. Our working hypothesis is to log and flag very old events instead of rejecting them automatically, until you have observed the real behavior at your volume.
How do you apply rate limits without losing messages?
Rate limiting a webhook has a trap: if you return an error to Meta because of excess volume, Meta retries, and the load comes back. Rejecting at the door is useful against unauthenticated traffic, but for authenticated traffic the limit should act further downstream.
Think in layers:
- Before the signature: limits on body size and read time. Huge bodies or slow connections are dropped early at minimal cost.
- Traffic without a valid signature: a counter per source and a temporary block for those who keep trying. Because Meta's source addresses can change, relying on a fixed IP list as your only protection is fragile. Use it as an optional reinforcement, not a replacement for the signature.
- After signature and deduplication: accept the event, respond quickly with success and put it in a queue. The real limit lives here.
- Per sender: a cap on messages processed per customer number in a time window. This contains loops (a bot on the other side talking to yours) and abuse that would generate model cost.
- Global: maximum concurrency of model calls and a daily usage budget with an alert. When the cap is reached, the agent replies with a short waiting message or hands off to a human.
The cap values depend on your flow, and there is no universal number. Start conservative, measure in the pilot and adjust. On the cost side, thinking in per-unit metrics helps define a reasonable cap per conversation, as covered in how to measure an AI automation pilot.
What about the message content once the signature has passed?
The signature guarantees origin and integrity, not intent. A real customer, or someone using a customer's number, can send text that tries to push the agent into ignoring rules, revealing data or taking an action. We always treat WhatsApp text as untrusted input.
OWASP maintains a list of the main risks for LLM applications, including prompt injection and excessive agent autonomy. The practical path is to limit what the agent can do (restricted tools, minimal permissions, human approval for sensitive actions) and to validate outputs and actions. We cover this in guardrails for AI agents. If you also expose an agent gateway, the OpenClaw Gateway security checklist follows the same idea of evidence before exposure.
On personal data: the payload contains a phone number, a profile name and text. Keep only what is necessary in logs, mask numbers where possible and define a retention period. ANPD is the institutional reference for LGPD guidance in Brazil.
Illustrative scenario: the same event arrives twice
Illustrative scenario: a store uses a WhatsApp agent to report order status. The server is slow to respond because of a slow ERP query, and Meta delivers the same message again.
Without protection, the agent processes both copies, queries the system twice and sends the customer two nearly identical replies. If the message were a rescheduling request, the system could record the action twice.
With the layers described here, the first request passes the signature check, stores the message ID and enters the queue. The second also passes the signature check, but the ID already exists, so it gets an immediate success response and is discarded. The log records a duplicate, without the message text. The agent replies once, and the rescheduling action carries its own key that blocks a second execution.
Review checklist before going to production
- The signature is validated over the raw body, with constant-time comparison.
- The app secret sits in a vault, differs between environments and has an owner for rotation.
- Requests without a valid signature are rejected before any parsing or lookup.
- Atomic deduplication by identifier exists, with a defined retention period.
- The endpoint responds quickly and processes the rest through a queue.
- There are limits on size and per sender, a maximum concurrency and a budget alert.
- Logs record the decision and the identifier, without the full text or the full number.
- Agent actions with external effects are idempotent and run with minimal permissions.
- A human handoff path exists for when a limit is reached.
How this work fits our method
In the method we follow, the webhook shows up in all four stages. In mapping, we draw the event path from Meta to the agent's action and list what each step can access. In limiting, we define signature, deduplication, caps and permissions. In piloting, we run a real flow with logs and an agreed success criterion, including tests for an invalid signature, a repeated event and a volume spike. In transferring, we deliver the runbook, the alerts and the decision to expand, adjust or stop. When the operation is managed, Be Human handles that follow-up.
For company-initiated sends, which follow their own consent and template rules, see WhatsApp outbound messages: templates and opt-in.
Frequently asked questions
Do I need to validate the signature if my endpoint uses HTTPS and a hard-to-guess URL? Yes. HTTPS protects traffic in transit, and an obscure URL is not a reliable secret, since it can leak through logs, configurations and monitoring tools. The signature is what proves Meta sent that exact body.
Which secret do I use to compute the signature? Use the App Secret of the app configured in Meta. The verify token serves only the initial GET handshake, and the access token is for calling the API, not for signing or validating notifications.
Does a valid signature prevent replay? No. A legitimate request that is resent still carries a correct signature, because the body is the same. Protection comes from deduplication by message ID, preferably with atomic writes and a retention period longer than the resend window.
Should I return an error when the rate limit is hit? For traffic without a valid signature, yes. For authenticated events, prefer to accept, queue and apply the limit during processing, because errors trigger new Meta retries and can increase the load. When the cap is reached, reply with a waiting message or hand off to a person.