What is a webhook: how Zalo, Facebook and forms notify a CRM, with signatures, retries and de-duplication

What a webhook is, how it differs from polling an API, and how to verify signatures, return 2xx fast, handle retries, de-duplicate and reconcile, based on Meta, Zalo, Stripe and GitHub documentation.
When a customer messages on Zalo, fills in a Facebook ad form or pays, the CRM needs to know at once without anyone retyping it. Systems usually notify each other by webhook, and most lost or duplicated leads come from how it is received, rarely from the sender.
This article covers signature checks, returning 2xx fast, retries, de-duplication, event order and monitoring. The main sources are documentation from Meta, Zalo for Developers, Stripe, GitHub, Shopify, Next.js and RFC 9110, checked on October 21, 2026.
Click Star already has articles on APIs and on unifying customer data from forms, Zalo, Facebook and calls. This one covers the part in between: how a platform's event enters a company's system safely and without duplicates.
Key points
- A webhook is how a platform sends an HTTP POST with JSON to a company's address when an event occurs. Meta says this spares you constant Graph API queries and rate limits.
- A webhook does not fully replace calling the API. Shopify writes that webhook delivery is not always guaranteed and recommends periodic reconciliation jobs.
- Check the signature on the raw body. Meta and GitHub use HMAC-SHA256 in X-Hub-Signature-256, Stripe adds a timestamp against replay, and Zalo's documented formula has its own shape.
- Waiting times are short: Zalo requires a 200 response within 2 seconds, Messenger 5 seconds or less, GitHub 10 seconds. The standard approach is to return 2xx first and process through a queue.
- Retries make events arrive twice and order is not guaranteed. Use a de-duplication key with a unique database constraint.

What a webhook is and how it differs from calling the API
A webhook is an HTTP request that one system sends to another system's address when an event occurs. According to GitHub (read October 21, 2026), webhooks deliver notifications to an external web server whenever certain events happen.
Calling an API on a schedule (polling) is the opposite: the company asks "anything new?" every minute or hour. It is simple, but the delay equals the interval and calls are spent even when nothing changed.
The 2 approaches divide the work, not replace each other. A webhook answers "what just happened", while the API answers "what is the full current state".
For Facebook lead forms, Meta's documentation describes 2 routes: subscribe to the leadgen field, and bulk-read with a GET on the form ID. It says real-time pings can lag by up to a few minutes, so do not promise a reply "within seconds" on webhooks alone.
What a webhook request looks like
Platforms look alike: an HTTP POST, a JSON body, headers describing the event, and a signature. Field names and signing differ.
- Meta: the body has object and an entry array (id, time, changes or messaging). In Messenger, sender.id is the PSID and message.mid is the message ID.
- Zalo: a message event has app_id, sender.id, event_name (for example user_send_text), message.msg_id, timestamp, and the header X-ZEvent-Signature.
- GitHub: headers X-GitHub-Event, X-GitHub-Delivery (the GUID of the delivery) and X-Hub-Signature-256. Payloads are capped at 25 MB.
- Stripe: a JSON body and the Stripe-Signature header. A "thin" event only signals the event; a "snapshot" carries a copy of the object.
Many notifications are a doorbell, not a parcel. Meta's leadgen notification carries leadgen_id, page_id, form_id, ad_id and created_time; the customer's name and phone number need 1 Graph API call and the leads_retrieval permission.
The journey of an event from platform to CRM

This is Click Star's reference framework, not any one platform's description. Only the first 3 steps fall inside the platform's short waiting time.
- An event occurs (a Zalo message, a form, a payment) and the platform sends an HTTP POST.
- The endpoint checks the signature against the raw body and rejects forged or stale requests.
- The endpoint writes the event verbatim to a raw events table, then returns 2xx.
- A background worker picks up unprocessed events and de-duplicates them by event key.
- It normalizes the data and, if the notification is thin, calls the API for the full data.
- It writes to the CRM or CDP, matches the existing profile and records the result.
Step 3 is the pivot: if the server dies after returning 2xx without saving, the event is lost because the platform considers it done. Save first, reply second.
Authentication: signatures and replay protection
A webhook address is public, so anyone who knows it can post to it. Stripe states that an attacker could send fake events to fulfil orders or modify records.
A signature solves this: both sides share a secret key, the sender attaches an HMAC (RFC 2104, a keyed hash) of the content, and the receiver recomputes and compares.
Meta. The header X-Hub-Signature-256 looks like sha256=..., computed as HMAC-SHA256 with the App Secret. Messenger's documentation notes the signature is generated from the escaped Unicode version of the payload in lowercase hex; hashing the decoded text gives a different signature.
GitHub. Also X-Hub-Signature-256. The documentation advises a constant-time comparison such as crypto.timingSafeEqual and avoiding the plain == operator, which can enable timing attacks.
Stripe. The Stripe-Signature header holds t= (timestamp) and v1= (signature). The official libraries default to a 5-minute tolerance against replay; a tolerance of 0 disables the freshness check.
Zalo. The documentation gives X-ZEvent-Signature with the formula mac = sha256(appId + data + timeStamp + OAsecretKey), where data is the JSON string of the request body. This is a concatenated hash, unlike the HMAC of Meta and GitHub.
Take the raw body before any JSON parser touches it: Stripe says any manipulation breaks verification. Use HTTPS; Meta rejects self-signed certificates.
Return 2xx fast, process later
The reply to a webhook is not the processing result, only "received". RFC 9110 describes the 2xx class as a request received, understood and accepted.
Waiting times are short and differ (read October 21, 2026):
- Zalo: respond with 200 OK, within 2 seconds at most.
- Messenger: respond with 200 OK within 5 seconds or less.
- GitHub: respond with 2xx within 10 seconds.
- Stripe: return 2xx before complex logic; a 3xx redirect counts as failure, so register the final address.
Stripe and GitHub both advise a queue; Stripe's reason is that a spike can overwhelm a server processing synchronously.
In Next.js on Vercel, the after function (stable since 15.1.0, documentation updated September 16, 2026) runs work after the response is sent, within the function's duration limit. Click Star's view: it suits logging, not a durable queue; save anything that must not be lost to the database first.
Retries, idempotency and de-duplication
Platforms retry on failure, so an event may arrive more than once. The policies differ:
- Zalo: resends unsuccessful events (its example is a failed connection) after 30 seconds, 5 minutes, 15 minutes, 30 minutes and 1 hour, with num_retry in the header. If it never recovers, the subscription is cancelled.
- Meta: retries immediately, then less often over 36 hours, then drops it. Messenger alerts after 15 minutes and unsubscribes after 1 hour.
- Stripe: up to 3 days with exponential backoff in live mode. Manual resend works up to 15 days (Dashboard) or 30 days (CLI).
- GitHub: does not redeliver automatically; you redeliver manually or with a script, using the API that lists deliveries.
So design for idempotency: processing twice gives the same result as once. Meta says your server should handle de-duplication, and Stripe advises logging processed event IDs.
Take the key from the platform: Stripe's event ID, GitHub's X-GitHub-Delivery (redeliveries keep the original value), Shopify's X-Shopify-Webhook-Id. Meta and Zalo give no common event ID in the pages read; message.mid and message.msg_id are candidates, by Click Star's inference.
Let the database decide: a raw events table with a unique constraint on (source, event key), inserted with ON CONFLICT DO NOTHING. PostgreSQL's documentation says ON CONFLICT guarantees an atomic outcome under high concurrency.
Event order is not guaranteed
Stripe writes that it does not guarantee delivery in the order events are generated; Shopify says the same. Messenger's documentation says messages may not arrive in the order sent and advises sorting by the timestamp field.
Stripe warns that created is in seconds, so events can share it. Meta notes that time is when the notification was sent, not when the change happened.
There are 3 ways to cope:
- Sort by the source's own timestamp (Shopify suggests X-Shopify-Triggered-At or updated_at in the payload).
- When an event arrives early, fetch the latest object through the API instead of trusting the event body (Stripe describes this).
- Overwrite only when the event's timestamp is newer than the stored one. This is Click Star's approach; the documentation does not prescribe it.
5 things every webhook receiver must do

Click Star condenses the above into 5 duties, each sourced earlier. Missing 1 means the other 4 cannot compensate: a good signature check with synchronous processing still times out.
Monitoring, logging and testing
Stripe shows each delivery as Delivered, Pending or Failed with the HTTP code; GitHub lists deliveries through the API.

A log should hold the event ID, source, receipt time, signature result, processing result and attempt count. Never log the secret key; consider masking phone numbers and emails. Track the 2xx rate, receipt-to-CRM time, queue length and reconciliation differences (Click Star's suggestion).
For testing, Meta says an app in development mode receives only test notifications, and Stripe offers the stripe trigger and stripe listen commands.
What this means for Vietnamese businesses
Zalo's documentation has easily missed points (read October 21, 2026). The webhook address should be an HTTPS domain, not host:port, and the Official Account must grant an Official_Account_Access_Token to the app.
The most misleading option is "Lọc cú pháp" (syntax filter): when on, the webhook receives only text messages that begin with "#". If the CRM is missing messages, check this first.
Click Star's view. A webhook is little code with large consequences: a rushed receiver can lose leads with no sign of failure.
For a small business, 1 raw events table with a unique constraint, 1 worker and 1 daily reconciliation job is often enough. On interest: Click Star offers integration, CRM and CDP services; this view rests on the documentation read, not on any project's measurements.
To do: a 5-day plan
- Day 1: list every event source with its signature header, waiting time, retry schedule and key.
- Day 2: build the HTTPS endpoint: raw body, signature check, raw events table with a unique constraint, return 2xx.
- Day 3: build the background worker with increasing retry intervals.
- Day 4: test the failure paths in a test environment: bad signature, duplicate, reordering.
- Day 5: write a periodic reconciliation job (Shopify suggests filtering by updated_at) and alert when errors rise or events go silent.
Common mistakes
- Doing heavy work inside the request before replying, exceeding the waiting time and triggering retries.
- Parsing the JSON before checking the signature, so the signature is computed on a reformatted body and always fails.
- Not de-duplicating, so each retry creates another customer profile or another order.
- Using arrival order or second-level timestamps to decide which event is newer.
- Relying on webhooks alone, with no reconciliation or alerts, and learning of failure only after the platform unsubscribes.
What is not known
Click Star has no independent measurement of webhook failure or delay rates in Vietnamese businesses. The deadlines and retry schedules are each platform's own, read on October 21, 2026, and may change.
Zalo's pages do not state the value format of X-ZEvent-Signature, the allowed time skew or a common event ID. Meta's general webhook page states no waiting time. Click Star could not read Zalo Notification Service's webhook page. Most pages were read through a summarizing tool, so check signature formulas on the original pages.
Related posts
Need tailored advice for your business?
Click Star helps you build a solution that fits your industry and scale. We answer every question during business hours.



