Everything you need to understand WordPress webhooks — how they work under the hood, how to set them up, where they break in production, and what a reliable delivery setup actually looks like.
A WordPress webhook is an outbound HTTP POST request sent automatically when something happens inside WordPress. A form gets submitted. An order status changes. A new user registers. WordPress fires an action hook, your code catches it, and sends a JSON payload to a URL you configured.
That's the happy path. In practice, the implementation details determine whether your webhook is reliable or a ticking time bomb.
Most webhook setups in WordPress rely on a single WordPress function called synchronously during the PHP request that triggered the event:
This runs inside the same PHP process that completed the order. The HTTP request is sent synchronously — the PHP thread sits and waits for a response before doing anything else. The user's browser is waiting too.
If the CRM is slow, the checkout is slow. If the CRM is down, the request times out after 5 seconds and the event is permanently lost. There is no queue. There is no retry. WordPress has already moved on.
The key insight: WordPress was built to render pages, not to deliver events reliably. Using it as an event delivery system without adding the right infrastructure is the source of most webhook failures.
There is no single "WordPress webhooks" feature. How you set one up depends on where the event comes from. The general flow is always the same:
add_action() or a plugin's built-in webhook featurewp_remote_post() or a plugin
Plugins like Gravity Forms, Contact Form 7, and WooCommerce all have their own
webhook/notification systems with varying levels of configurability.
Custom plugins and themes typically use add_action() hooks directly.
Gravity Forms has a built-in Webhooks Add-On. Configure endpoint URL, request format, and field mapping per-form. No custom code needed for basic setups.
Full setup guide + reliability fix →CF7 requires a plugin or custom code to send webhooks. The CF7 to Webhook plugin is the common solution — it adds endpoint configuration to each form.
Full CF7 webhook example →WooCommerce has a native webhook system under WooCommerce → Settings → Advanced → Webhooks. It covers order, product, customer, and coupon events. Delivery is handled via Action Scheduler, so it is asynchronous with basic retry support — but logging and replay are limited compared to a dedicated delivery layer.
Most WordPress webhook use cases fall into one of these four categories. Notice that all of them are business-critical flows — which makes silent failures a serious problem.
Form submissions — Gravity Forms, CF7, WPForms — pushed to HubSpot, Pipedrive, or a custom API. A missed event means a missed lead. No second chance.
WordPress as an event source for n8n, Make, Zapier, or Pipedream. Order placed → Slack notification → fulfillment ticket created. One dropped webhook breaks the whole chain.
Push order events to ERP systems, 3PL providers, accounting platforms. Order fulfillment depends on the event arriving. A missed delivery means a missed shipment.
Push WordPress events to Notion, Airtable, Slack, custom SaaS APIs. Any HTTP endpoint can be a webhook consumer — making WordPress a flexible event emitter for your entire stack.
If any of these flows matter to your business, you cannot afford fire-and-forget delivery. The question is not if a webhook will fail — it's when, and what happens next.
Most WordPress webhook setups are "fire and forget" — which really means "hope it works." When conditions are perfect, they do. In production, conditions are never perfect.
The silence is the failure mode.
No error in the WordPress admin. No email. No alert.
The order shows as completed, the form shows as submitted — but your CRM, ERP, or automation platform never received the event.
These are not edge cases. They are the normal operating conditions of any production environment. External APIs have planned maintenance. Networks drop packets. Rate limits kick in. A webhook system that can't handle any of these is not production-ready.
The downstream effects of a missed webhook depend entirely on what the webhook was doing. They range from annoying to genuinely damaging.
A contact form submission that never reaches your CRM is a prospect who falls through the cracks. Sales teams never see them. Follow-ups never happen.
A WooCommerce "order completed" event that never reaches your ERP means an order that never gets fulfilled, or an invoice that never gets created.
Automation workflows in n8n, Make, or Zapier are only as reliable as the trigger event. One dropped webhook breaks the entire downstream sequence silently.
The hardest part: you often won't know something went wrong until a customer complains or you manually audit records. By then, recovery is difficult.
The consistent pattern across all these failures: the problem is discovered days later, when someone notices a discrepancy. By then, PHP logs have rotated, the original payload is gone, and there is no forensic record to work with.
The core architectural decision in webhook delivery is when the HTTP request gets sent: during the PHP request that triggered the event (synchronous), or in a background process (asynchronous).
Async delivery gives you two things that synchronous delivery can never provide:
The tradeoff is latency — async delivery introduces a small delay between event and delivery (typically seconds to a minute). For most use cases, this is completely acceptable. For use cases where it isn't, synchronous delivery can still be used selectively while still adding logging and idempotency.
For a detailed walkthrough of async dispatch architecture in WordPress, see Async Webhooks in WordPress →
Reliable webhook delivery is not about writing better PHP. It is about adding the right infrastructure around the HTTP call. A production-grade setup needs five components:
Store events in the database before attempting delivery. This decouples event capture from dispatch — the event survives even if the delivery process crashes mid-flight.
Retry failed deliveries automatically. Space retries further apart with each attempt: 1 min → 2 min → 4 min → 8 min. Retry 5xx and network errors; fail permanently on 4xx — those are configuration problems, not transient failures.
Send events from a background worker process, not during the user-facing request. This removes the performance risk and allows the worker to handle long-running deliveries safely.
Log every attempt: timestamp, endpoint, HTTP status, response body, duration. Logs are what turn invisible failures into diagnosable problems — and they're the foundation for replay.
After an outage or configuration problem, you need to be able to resend failed events from the delivery log. Without replay, any failure window means permanent data loss.
These five components are not optional extras. Each one addresses a specific failure mode. Remove any one of them and you reintroduce that failure mode. Deep dive: building a retry and replay system for WordPress →
WordPress was not designed as event infrastructure, and that shows in several ways. Understanding the limitations helps you work around them effectively.
WP-Cron — WordPress's built-in job scheduler — does not run on a time-based schedule. It fires on page load. If your site receives no traffic overnight, WP-Cron does not run. Events that should have been retried at 2am sit in the queue until morning.
The reliable fix: disable WP-Cron's page-load trigger and replace it with a real system cron job that runs every minute. Full breakdown of WP-Cron limitations and fixes →
PHP is stateless and request-bound. Each page load boots PHP fresh, runs, and terminates. There is no persistent background process. This means background jobs must be triggered externally — you cannot have a long-running PHP worker without additional infrastructure.
On shared hosting, execution time limits (often 30–60 seconds) can kill a batch processing job mid-run, leaving some events in a "processing" limbo state. Your queue needs a stuck-job detection mechanism to recover from this.
WordPress core has no persistent job queue. The common solutions are:
Action Scheduler is a solid general-purpose queue, but it requires custom integration code to handle all the delivery concerns (status tracking, per-attempt logging, idempotency keys). A purpose-built solution handles these for you.
Each row in this table represents a specific failure mode. The fire-and-forget column is missing all the things that let you detect, recover from, and prevent delivery failures.
| Capability | Fire-and-forget | Production-grade |
|---|---|---|
| Execution model | Synchronous, blocks PHP | Background worker |
| Event persistence | None — lost on crash | Database queue before dispatch |
| Retry on failure | Never | Automatic with exponential backoff |
| Retry logic | N/A | 5xx + network errors only; 4xx → permanent fail |
| Delivery logging | None | Per-attempt: status, body, duration |
| Event replay | Not possible | Manual resend from delivery log |
| Idempotency | None | UUID on every event, stable across retries |
| Performance impact | Blocks user-facing request | Zero — async dispatch |
| Failure visibility | Silent | Logged, queryable, alertable |
Debugging a fire-and-forget webhook implementation is painful: there is nothing to debug with. A production-grade setup makes debugging straightforward.
If you have per-attempt logs, debugging is usually a five-second task: find the event, look at the HTTP status and response body, and you know exactly what went wrong. A 401 means authentication. A 400 means payload format. A 503 means the API was down.
The most common source of 400 errors is a payload structure mismatch — the receiving API expects a different field name, nesting, or data type. Log the full request body on every attempt. When something breaks, you can inspect exactly what was sent.
Before debugging the WordPress side, verify the endpoint works. Use Hoppscotch or curl to send a test payload directly to the endpoint URL and confirm it responds correctly. This isolates whether the problem is WordPress-side or endpoint-side.
If your setup supports it, manually replay the failed event after fixing the underlying cause. This is the only way to recover events without asking the user to resubmit a form or re-trigger an order event.
webhook.site gives you a temporary URL that logs every incoming request in full detail — headers, body, timing. Invaluable for inspecting exactly what WordPress is sending before you point it at a real endpoint.
Most of these follow directly from the failure modes described above. They're not optional for anything you run in production.
version field so receivers can handle schema evolution gracefullyThe single highest-leverage change you can make: add logging before anything else. You cannot fix what you cannot see. Even if you don't implement the full queue-and-retry system immediately, logging every delivery attempt gives you the visibility to understand where and when things are failing.
These guides walk through complete webhook setups — including the reliability layer. Not just "how to configure the webhook URL," but how to handle failures, log deliveries, and recover from outages.
wp_remote_post() called synchronously during page execution. There is no retry mechanism, no persistent queue, and no logging. If the receiving API is unavailable — timeout, 500 error, rate limit — the request fails and the event disappears permanently with no record that anything went wrong.
The WordPress Webhook Plugin adds a production-grade delivery layer — persistent queue, automatic retry, full delivery logs, and replay — without changing how your existing integrations work. Free and open source.