TL;DR: An API answers when you ask. A webhook tells you without being asked. They are not rivals: a webhook is an HTTP request that the other system makes to you, and most integrations that hold up in production use both.
- API call (pull): your code requests data when it wants it. You control timing, you pay in requests, and you only learn about a change on your next poll.
- Webhook (push): the other system POSTs to your URL when something happens. It arrives within seconds and costs nothing while nothing happens, but you must be reachable, answer fast, and cope with duplicates.
- Senders differ widely. GitHub waits 10 seconds and never retries on its own. Shopify waits 5 seconds and retries 8 times in 4 hours. Stripe retries for up to 3 days.
- The durable pattern: the webhook is the signal, the API is the source of truth, and a periodic API check catches whatever the webhooks missed.
/ Definition
What is the difference between a webhook and an API?
The direction of the first request. With an API, your system asks: GET /orders/1042, and the other side answers. With a webhook, the other system speaks first: when order 1042 is paid, it sends an HTTP POST describing that event to a URL you registered in advance.
That makes "webhook vs API" a slightly misleading pair. A webhook is not an alternative technology. It is an HTTP request, usually JSON, sent by one system's API layer to another system's endpoint. GitHub's documentation describes the choice in exactly those terms: you can subscribe to webhooks "as opposed to polling an API (calling an API intermittently) to see if data is available".¹
The practical difference is who carries the work and who carries the risk:
- With polling, the client does the work on every request, including all the requests that find nothing new. Timing is under the client's control, and a missed poll is simply repeated later.
- With a webhook, the sender does the work, but only once per event. Timing is under the sender's control, and whether a failed delivery is ever retried depends entirely on the sender's rules.
The second point is where most integration bugs live, and the section on delivery rules below puts real numbers on it.
/ Polling
Why do API providers tell you to use webhooks instead of polling?
Because polling spends rate limit on questions whose answer is usually "nothing changed". GitHub's REST API best practices are blunt: "You should subscribe to webhook events instead of polling the API for data. This will help your integration stay within the API rate limit."²
The arithmetic shows why. An authenticated GitHub user gets 5,000 requests per hour. Checking one resource once a minute costs 60 requests an hour, so:
- 5,000 ÷ 60 = 83 resources is the most you can watch at one-minute freshness before the limit stops you.
- Relaxing to a five-minute interval buys 5,000 ÷ 12 = 416 resources, but the average change now waits 2.5 minutes before you notice it, and the worst case waits 5.
- If a resource changes twice a day, 1,438 of its 1,440 daily checks, 99.9%, return nothing new.
A webhook subscription for the same 83 resources costs zero requests while nothing happens and delivers each change within seconds. GitHub's own summary: "Webhooks require less effort and less resources than polling an API. Webhooks scale better than API calls."¹
Polling is not wrong, though. It is predictable, needs no public endpoint, and survives your server being down, because the data waits on the provider's side until you ask again. Those properties come back later in this article, because they are the ones webhooks lack.
/ Delivery
What does a webhook sender actually promise?
Less than most integrations assume, and different things for each sender. Five first-party rulebooks, side by side:
| Sender | Your response deadline | Retries on failure | When it gives up |
|---|---|---|---|
| GitHub | 2xx within 10 s | None automatic. Redelivery is manual or your own script | Immediately |
| Stripe | "Quickly", no number published | Exponential backoff for up to 3 days (live mode) | After 3 days; manual resend for 30 days |
| Shopify | Respond within 5 s | Up to 8 times in a 4-hour period | After 8 failures, and the subscription is removed |
| WooCommerce core | Sender waits up to 60 s | None: a failure only increments a counter | Webhook disabled after more than 5 consecutive failures |
| Standard Webhooks (spec) | Recommends a 15–30 s timeout | Exponential schedule spanning multiple days | Example schedule ends after about 75 hours |
Three consequences follow from that table.
A slow receiver is a failing receiver. If your endpoint does the real work, such as writing to a database, calling a CRM or sending an email, before it answers, a busy moment pushes it past Shopify's 5 seconds and the delivery counts as failed even though the work eventually finished. Stripe's guidance is to return a 2xx "before any complex logic that might cause a timeout". Acknowledge first, process in a queue.
"No retry" really means no retry. A GitHub delivery that hits a deploy restart is gone unless you notice and redeliver it. Stripe's three-day window hides short outages; GitHub's and WooCommerce's hide none.
Some senders disable you. Shopify removes the subscription after eight failures in four hours. WooCommerce sets the webhook to disabled after more than five consecutive failures, and a failure there is anything other than 2xx, 301 or 302.³ Neither tells your endpoint it happened. An endpoint that broke on Friday can stop receiving anything at all by Saturday, and everything looks quiet.
/ Duplicates
Why can the same webhook arrive twice or out of order?
Because at-least-once delivery is the only guarantee a sender can realistically make. If your server processed the event but the 200 response was lost in transit, the sender cannot tell that apart from a failure, so it sends again. Every major sender says so in writing:
- Stripe: "Webhook endpoints might occasionally receive the same event more than once", and it "doesn't guarantee the delivery of events in the order that they're generated."
- Shopify: "your app might receive the same webhook more than once, for example after a network timeout or a retry", and a
products/updatecan arrive before theproducts/createit follows (verify deliveries). - GitHub: webhooks may be delivered "in a different order than the order in which the events took place" (troubleshooting webhooks).
The fix is the same everywhere: every delivery carries a unique id that stays the same across retries. Stripe has the event id, Shopify sends X-Shopify-Webhook-Id, GitHub keeps X-GitHub-Delivery identical on a redelivery, and the Standard Webhooks spec names it webhook-id and says outright that it "is often used as an idempotency key". Store the ids you have processed, and skip an id you have seen. For ordering, compare the timestamp inside the payload with the one you stored, and discard updates older than what you already have.
Your app shouldn't rely on receiving data from Shopify webhooks. Webhook delivery isn't always guaranteed. — Shopify developer documentation
/ Choosing
When is an API call the better choice?
Whenever you need an answer about the present, rather than a notice about the past. Use the API when:
- You need the data right now. A checkout page that shows live stock asks the inventory API. It cannot wait for an event that may never come.
- You need the current state, not a change. "What is this customer's plan today?" is an API question. Rebuilding it from a stream of webhooks is fragile, since one missed event corrupts the answer for good.
- You cannot receive requests. A laptop script, a site behind a firewall or a staging environment on
localhosthas no public URL, and polling needs none. - The provider sends no webhook for that event. Many APIs cover only some of their objects with events. Polling is the only option for the rest.
- You are backfilling. Webhooks start at the moment you subscribe. Six months of existing orders come from the API, one page at a time.
Use a webhook when something needs to happen because something else happened: send the order to the warehouse, notify the team, add the buyer to a list. That is most integration work, which is why webhooks feel like the default.
/ Both
Why do production integrations use webhooks and the API together?
Because each covers the other's failure. Shopify's documentation makes the point directly: delivery is not guaranteed, so apps should "use reconciliation jobs to periodically fetch data from Shopify".⁴ The pattern has three parts.
- Acknowledge fast. Verify the signature, record the event id, answer 2xx, and put the work on a queue. The sender's deadline is now easy to meet.
- Treat the payload as a hint. For anything where freshness matters, have the worker fetch the object from the API by the id in the payload. If two updates arrived out of order, both workers read the same current state, and the ordering problem disappears.
- Reconcile on a schedule. Once an hour or once a night, ask the API for everything changed since the last run and process whatever the webhooks missed. Because this runs rarely, it costs a handful of requests instead of thousands.
With webhooks carrying the traffic, the poll only has to catch the rare miss. Run the numbers from the polling section again: 83 resources checked every minute cost 4,980 requests an hour. An hourly "changed since" query costs one request plus pagination.
/ WordPress
How do webhooks and APIs work on a WordPress site?
WordPress is on both sides of the line. As an API, core ships the WordPress REST API, and any plugin can add a route that receives webhooks from Stripe, a form service or n8n. As a sender, core has a REST API but no settings screen for outgoing webhooks. WooCommerce adds one under Settings → Advanced → Webhooks for store topics such as orders, products and customers.
WooCommerce's core webhooks are worth understanding before you rely on them, and the source is clearer than the settings screen. Deliveries are queued through Action Scheduler by default. Each one is signed with X-WC-Webhook-Signature, a base64 HMAC-SHA256 of the body. The request waits up to 60 seconds and follows no redirects. And a failed delivery is never retried: it only counts toward the disable threshold. Anything outside the store topics, like a form submission, a membership change or a booking, needs code.
That code is usually wp_remote_post() inside the plugin's action hook. It works, with two defaults to know. It runs synchronously, so the visitor waits for the remote server. And its default timeout is 5 seconds, after which the request fails once and is forgotten. The sections above apply in reverse: now you are the sender, and every rule you wanted from Stripe, you have to write yourself.
| Sender duty | wp_remote_post in a hook | Webhook Actions |
|---|---|---|
| Which events | Any hook you write code for | Any WordPress or plugin hook, chosen in the admin |
| Where the request runs | Inside the page load, visitor waiting | Queued in its own table, delivered in the background |
| Receiver is down or slow | One attempt, 5-second default timeout | Retried on 5xx and 429 with exponential backoff, 5 attempts by default |
| Receiver rejects the payload | Invisible unless you log it | Marked permanently failed, response body in the delivery log |
| Duplicate detection downstream | Build your own id | X-Event-Id and X-Event-Timestamp on every delivery |
| A missed delivery | Gone | Replayed from the log |
| Signed requests | Your own hash_hmac call | Not built in: HMAC needs a filter on the headers |
Seeing it run beats reading about it. The live preview boots a throwaway WordPress with Webhook Actions already installed and demo deliveries sitting in the log — no signup, nothing left on your machine afterwards.
/ Exposure
What does a webhook not protect you from?
Forged requests. Your webhook URL is public, and anyone who finds it can POST to it. Verify the signature on every request, using the raw body before any parsing: Stripe-Signature, X-Shopify-Hmac-SHA256, X-WC-Webhook-Signature. A receiver that skips this will act on whatever an attacker sends.
Replays. A valid signed request captured once stays valid forever unless you check its timestamp. Stripe's libraries reject events older than 5 minutes by default, and the Standard Webhooks spec signs the id and timestamp with the body for the same reason.
The quiet failure. A disabled WooCommerce webhook or a removed Shopify subscription looks, from the receiving side, exactly like a quiet day. Alert on silence: if a stream that normally delivers hourly has delivered nothing since yesterday, something upstream has given up. This is the most common way webhook integrations fail, and it rarely shows up as an error.
Your own retries. When you are the sender, retrying a 400 changes nothing and retrying a 500 without backoff helps overload the receiver you are waiting on. Retry 429 and 5xx, back off exponentially, and stop at a limit. The retry policy article covers the schedule and what happens after the last attempt.
woocommerce_max_webhook_delivery_failures filter, default 5, and the check is "more than". All retry and timeout figures in the table were read from each sender's documentation on 2026-09-28.