REST API & Integrations

Webhooks in WordPress: Sending and Receiving

How to send webhooks from WordPress without slowing the site, and how to receive them safely — signature verification, idempotency, retries and debugging.

By 6 min read
Network diagram with a central WordPress node linked to connected services

A webhook is just an HTTP request one system makes to another when something happens. Simple in concept, and the place where integrations most often fail in production — usually because the failure modes weren't designed for.

Here's how to do both directions properly in WordPress.

Why webhooks instead of polling

Polling means asking repeatedly whether anything changed. It's wasteful — most requests return nothing — and it's always either too slow or too frequent.

A webhook inverts it: the system that knows something happened tells you immediately. One request instead of thousands, and near-real-time instead of up to one interval late.

The trade-off is that you now depend on delivery. A polled integration recovers automatically from a missed cycle; a webhook that isn't delivered is simply lost unless the sender retries. That's why retry and reconciliation matter more here than anywhere else.

Sending webhooks from WordPress

The trigger is usually a hook: a post published, an order status changed, a user registered, a form submitted. You attach a callback and send an HTTP request to the receiver.

The critical rule is that this must not happen inside the request that triggered it. If publishing a post makes a remote call, then publishing takes as long as that remote server takes — and if the server is down, publishing hangs until the timeout.

Queue it instead. Schedule the send as a background task and return immediately. WordPress's scheduled events work for low volume; the action scheduler is more robust if it's available.

Designing the outgoing payload

  • Include an event type and a version. The receiver needs to know what happened and how to interpret the shape, and you'll want to change the shape one day.
  • Include a unique event ID, so the receiver can deduplicate if you retry.
  • Include a timestamp, so out-of-order delivery can be detected.
  • Send enough for the receiver to act without calling back for more, but don't send your whole database. A post ID plus the fields that matter is usually right.
  • Sign it, so the receiver can verify it came from you — an HMAC over the body with a shared secret, in a header.
  • Don't put secrets or unnecessary personal data in the payload. It's travelling over the internet to a system you may not control.

Retries on the sending side

Receivers go down, deploy, rate limit and time out. A webhook system without retries loses events during every one of those.

Retry with exponential backoff — seconds, then minutes, then longer — up to a bounded number of attempts. Distinguish retryable failures (timeouts, 500s, 429s) from permanent ones (400, 404), because retrying a malformed payload will fail identically forever.

After the final attempt, record the failure somewhere visible rather than discarding it. Someone needs to be able to see what wasn't delivered and replay it.

Receiving webhooks in WordPress

Register a REST route for it rather than using a page template or admin-ajax. You get routing, method handling and consistent error responses for less code.

The permission callback is the interesting part. The caller isn't a WordPress user, so capability checks don't apply — authentication is the signature instead. Validate it in the permission callback so an unsigned request never reaches your handler.

  • Verify the signature before doing anything else, using a constant-time comparison so the check can't be attacked by timing.
  • Check the timestamp and reject anything too old, which prevents a captured request being replayed later.
  • Validate the payload shape before acting on it.
  • Respond quickly — a 200 as soon as you've accepted the event — and do the actual work in the background. Many senders time out in a few seconds and will retry, producing duplicates.
  • Rate limit the endpoint. It's public by necessity.

Idempotency

You will receive the same event twice. The sender retried after a timeout your side actually processed, or a network hiccup produced a duplicate delivery. If your handler creates a record each time, you now have duplicates in your data.

Store the event IDs you've processed and ignore repeats. A transient or a small custom table both work; the expiry only needs to cover the sender's retry window.

Where you can, make the operation itself idempotent — update-or-create on a natural key rather than always create. That's more robust than relying on deduplication alone.

Debugging

Webhooks are hard to debug because you can't easily watch them arrive. Build for that from the start.

  • Log every received webhook: the headers, the payload, the signature check result and what you did with it. Prune the log on a schedule so it doesn't grow forever.
  • Log every sent webhook: the destination, the payload, the response status and the attempt number.
  • Provide a way to replay a specific event, which turns 'it didn't work' into a thirty-second test.
  • During development, use a request-inspection service to see exactly what a third party is sending before you write the handler.
  • Test locally with a tunnelling tool so a remote sender can reach your development machine.

Common failure modes

  • A security plugin or firewall blocking the incoming request, so the sender's dashboard shows failures your logs know nothing about.
  • A caching layer caching the webhook endpoint's response, so the sender gets a 200 without your code running.
  • The endpoint under a path that requires authentication site-wide.
  • A payload larger than the server's limits, silently truncated.
  • The receiver's response taking too long, so the sender times out and retries something you already processed.
  • A shared secret rotated on one side only.

Reconciliation

Even with retries and logging, events get lost — a sender that gave up, an outage longer than the retry window, a bug that dropped one.

So don't treat webhooks as the only path for data that matters. Periodically reconcile: fetch the last day's records from the source system and compare against what you have. It catches the gaps, and it's the difference between an integration you trust and one you hope is working.

For anything financial, reconciliation isn't optional.

Frequently asked questions

How do I send a webhook from WordPress?

Hook the event you care about — post published, order status changed — and send an HTTP request from a queued background task, never inside the triggering request. If you send synchronously, publishing a post takes as long as the remote server takes, and hangs entirely if it's down.

How do I verify an incoming webhook is genuine?

Check the signature the sender includes, usually an HMAC over the request body with a shared secret, using a constant-time comparison. Do it in the REST route's permission callback so unsigned requests never reach your handler, and reject requests with old timestamps to prevent replays.

Why is my webhook endpoint not receiving requests?

Most often a security plugin or firewall blocking the request before WordPress sees it — the sender logs a failure and your logs show nothing. Also check the endpoint isn't being cached, isn't behind site-wide authentication, and that the payload isn't exceeding a server limit.

How do I stop duplicate webhook events being processed twice?

Store the event IDs you've handled and ignore repeats — a transient or small table covering the sender's retry window is enough. Better still, make the operation itself idempotent: update-or-create on a natural key rather than always creating. Duplicates are inevitable, so design for them.

Should I use webhooks or polling for a WordPress integration?

Webhooks where the source supports them — one notification beats thousands of empty poll requests, and it's near real-time. The catch is that undelivered events are simply lost, so add retries on the sending side and periodic reconciliation against the source for anything that matters.

Topics

  • WordPress webhooks
  • webhook security
  • WooCommerce webhooks
  • WordPress integration