---
title: "Webhook vs API: The Difference and When to Use Each"
description: "Webhook vs API explained with the real delivery rules of GitHub, Stripe, Shopify and WooCommerce: timeouts, retries, duplicates, and when to poll instead."
url: "https://wpwebhooks.org/blog/webhook-vs-api/"
date: "2026-09-23"
---

# Webhook vs API: The Difference and When to Use Each

**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](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api). 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](https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks) | 2xx within 10 s | **None automatic.** Redelivery is manual or your own script | Immediately |
| [Stripe](https://docs.stripe.com/webhooks) | "Quickly", no number published | Exponential backoff for up to 3 days (live mode) | After 3 days; manual resend for 30 days |
| [Shopify](https://shopify.dev/docs/apps/build/webhooks/troubleshoot) | Respond within 5 s | Up to 8 times in a 4-hour period | After 8 failures, and the subscription is removed |
| [WooCommerce core](https://woocommerce.com/document/webhooks/) | Sender waits up to 60 s | None: a failure only increments a counter | Webhook disabled after more than 5 consecutive failures |
| [Standard Webhooks](https://www.standardwebhooks.com/) (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/update` can arrive before the `products/create` it follows ([verify deliveries](https://shopify.dev/docs/apps/build/webhooks/verify-deliveries)).
-   GitHub: webhooks may be delivered "in a different order than the order in which the events took place" ([troubleshooting webhooks](https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/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](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md) 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.](https://wpwebhooks.org/blog/webhook-vs-api/og_image.jpg)

/ 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](https://developer.wordpress.org/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](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/class-wc-webhook.php) 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()](https://developer.wordpress.org/reference/functions/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](https://developer.wordpress.org/reference/classes/wp_http/request/), 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 |

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.

[Try the live preview →](https://playground.wordpress.net/?blueprint-url=https://wpwebhooks.org/blueprint.json) [Install plugin](https://downloads.wordpress.org/plugin/flowsystems-webhook-actions.zip)

/ 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](https://wpwebhooks.org/blog/why-wordpress-webhooks-silently-fail-in-production/), 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](https://wpwebhooks.org/blog/webhook-retry-policy-exponential-backoff/) covers the schedule and what happens after the last attempt.

/Footnotes

¹ GitHub Docs, [About webhooks](https://docs.github.com/en/webhooks/about-webhooks), section "Choosing webhooks or the REST API".

² GitHub Docs, [Best practices for using the REST API](https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api), section "Avoid polling".

³ WooCommerce, [Webhooks documentation](https://woocommerce.com/document/webhooks/); 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](https://shopify.dev/docs/apps/build/webhooks), on delivery guarantees and reconciliation jobs.

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"Webhook vs API: The Difference and When to Use Each","description":"Webhook vs API explained with the real delivery rules of GitHub, Stripe, Shopify and WooCommerce: timeouts, retries, duplicates, and when to poll instead.","datePublished":"2026-09-23","dateModified":"2026-09-23","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/webhook-vs-api/","image":{"@type":"ImageObject","url":"https://wpwebhooks.org/blog/webhook-vs-api/og_image.jpg","width":1200,"height":630,"caption":"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."},"keywords":["webhook vs api","api vs webhook","difference between webhook and api","webhook vs polling","what is a webhook","webhook vs rest api"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"Webhook vs API: The Difference and When to Use Each","item":"https://wpwebhooks.org/blog/webhook-vs-api/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What is the difference between a webhook and an API?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Is a webhook faster than polling an API?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Do webhooks retry if my server is down?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Why did I receive the same webhook twice?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Should I use webhooks or the API?","acceptedAnswer":{"@type":"Answer","text":"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."}}]}

{"@context":"https://schema.org","@type":"ImageObject","contentUrl":"https://wpwebhooks.org/diagrams/webhook-vs-api.png","caption":"FIG 01 — Webhook as the signal, API as the source of truth, reconciliation as the safety net","description":"An event happens in the other system and it sends a webhook POST to your endpoint. The receiver verifies the signature, records the delivery id, answers 2xx within the sender deadline and puts the work on a queue. A delivery id seen before is skipped as a duplicate. The worker fetches the current state of the object from the API by its id and applies it, which makes out-of-order deliveries harmless. Separately, a scheduled reconciliation job asks the API for everything changed since its last run and processes whatever the webhooks missed.","encodingFormat":"image/png","creator":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org/"},"copyrightHolder":{"@type":"Organization","name":"Flow Systems","url":"https://flowsystems.pl/"},"copyrightNotice":"© Flow Systems","creditText":"WP Webhooks","license":"https://creativecommons.org/licenses/by/4.0/","acquireLicensePage":"https://wpwebhooks.org/image-license/"}
```
