WP Webhooks / Blog / Architecture

Webhooks and APIs: Push, Pull, and Why Real Integrations Use Both

Webhook vs API explained with the real delivery rules of GitHub, Stripe, Shopify and WooCommerce: timeouts, retries, duplicates, and when to poll instead.

9 min 2026-09-23
#webhooks#api#architecture

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:

SenderYour response deadlineRetries on failureWhen it gives up
GitHub2xx within 10 sNone automatic. Redelivery is manual or your own scriptImmediately
Stripe"Quickly", no number publishedExponential backoff for up to 3 days (live mode)After 3 days; manual resend for 30 days
ShopifyRespond within 5 sUp to 8 times in a 4-hour periodAfter 8 failures, and the subscription is removed
WooCommerce coreSender waits up to 60 sNone: a failure only increments a counterWebhook disabled after more than 5 consecutive failures
Standard Webhooks (spec)Recommends a 15–30 s timeoutExponential schedule spanning multiple daysExample 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/update can arrive before the products/create it 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 localhost has 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.

FIG 01 — Webhook as the signal, API as the source of truth, reconciliation as the safety net
  1. 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.
  2. 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.
  3. 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.

Cyberpunk illustration of two figures working a railway pump handcar on an elevated track above a rainy night city, one throwing their weight down on the beam while the other hauls the far end up.

/ 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 dutywp_remote_post in a hookWebhook Actions
Which eventsAny hook you write code forAny WordPress or plugin hook, chosen in the admin
Where the request runsInside the page load, visitor waitingQueued in its own table, delivered in the background
Receiver is down or slowOne attempt, 5-second default timeoutRetried on 5xx and 429 with exponential backoff, 5 attempts by default
Receiver rejects the payloadInvisible unless you log itMarked permanently failed, response body in the delivery log
Duplicate detection downstreamBuild your own idX-Event-Id and X-Event-Timestamp on every delivery
A missed deliveryGoneReplayed from the log
Signed requestsYour own hash_hmac callNot built in: HMAC needs a filter on the headers
try_it

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.

/Footnotes
¹ GitHub Docs, About webhooks, section "Choosing webhooks or the REST API".
² GitHub Docs, Best practices for using the REST API, section "Avoid polling".
³ WooCommerce, Webhooks documentation; the threshold is the 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.
⁴ Shopify, About webhooks, on delivery guarantees and reconciliation jobs.
FAQ

Things engineers always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

What is the difference between a webhook and an API? +
The direction of the first request. With an API, your system asks for data when it wants it. With a webhook, the other system sends an HTTP POST to a URL you registered when an event happens. A webhook is not a separate technology: it is an HTTP request made by one system to another system's endpoint.
Is a webhook faster than polling an API? +
Usually. A webhook arrives within seconds of the event, while polling only notices a change on the next request. Polling every five minutes means an average delay of two and a half minutes. Polling faster costs rate limit: at one request a minute, a 5,000-requests-per-hour limit covers only 83 resources.
Do webhooks retry if my server is down? +
It depends entirely on the sender. Stripe retries with exponential backoff for up to three days in live mode, Shopify retries up to eight times over four hours and then removes the subscription, and GitHub does not retry automatically at all. WooCommerce core webhooks do not retry either, and disable themselves after more than five consecutive failures.
Why did I receive the same webhook twice? +
Because senders deliver at least once. If your server processed an event but the response was lost, the sender cannot tell that from a failure and sends it again. Stripe, Shopify and GitHub all document duplicates and out-of-order delivery. Store each delivery's unique id, such as the Stripe event id or the X-Shopify-Webhook-Id header, and skip ids you have already processed.
Should I use webhooks or the API? +
Both, in most production integrations. Use the webhook as the signal that something changed, fetch the current state from the API when freshness matters, and run a periodic reconciliation query to catch any events that were never delivered. Use the API alone when you need data on demand, cannot expose a public URL, or are backfilling history.
Ready

Your next automation is
one sentence away.

$ wp plugin install flowsystems-webhook-actions --activate