WordPress Webhooks:
Setup, Examples, and Why They Fail
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.
How WordPress webhooks actually work
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:
typical wordpress webhook call
add_action( 'woocommerce_order_status_completed', function( $order_id ) { wp_remote_post( 'https://your-crm.example.com/webhook', [ 'headers' => [ 'Content-Type' => 'application/json' ], 'body' => wp_json_encode( [ 'order_id' => $order_id ] ), 'timeout' => 5, ] ); // Result never checked. No retry. No log. If it fails, no one knows. } );
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.
Setting up webhooks in WordPress
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:
- → Identify the trigger event — a form submission, order update, user action
- → Hook into the event — via
add_action()or a plugin's built-in webhook feature - → Define the endpoint URL — the external system that will receive the payload
- → Shape the payload — decide what data to send and in what format
- → Send the HTTP request — using
wp_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
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 →Contact Form 7
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
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.
WooCommerce order webhook example →Common use cases for WordPress webhooks
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.
Send leads to CRM
Form submissions — Gravity Forms, CF7, WPForms — pushed to HubSpot, Pipedrive, or a custom API. A missed event means a missed lead. No second chance.
Trigger automation workflows
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.
Sync WooCommerce orders
Push order events to ERP systems, 3PL providers, accounting platforms. Order fulfillment depends on the event arriving. A missed delivery means a missed shipment.
Custom API integrations
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.
Why WordPress webhooks fail silently in production
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.
- ✕ No retry mechanism — a single failed request means the event is gone permanently
- ✕ Synchronous execution — the HTTP request blocks the PHP process; slow endpoints slow down your site
- ✕ Timeouts — default timeouts are 5–10 seconds; any API under load will exceed this
- ✕ No persistent queue — events are not stored before dispatch; a PHP crash loses the event entirely
- ✕ No logging — WordPress does not log outbound HTTP requests by default; failures are invisible
- ✕ External API downtime — maintenance windows, rate limits, and outages are normal; your webhook will hit them
- ✕ No replay — even if you notice a failure, you cannot resend the original payload
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.
What actually happens when a webhook fails
The downstream effects of a missed webhook depend entirely on what the webhook was doing. They range from annoying to genuinely damaging.
Lost leads
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.
Missed order events
A WooCommerce "order completed" event that never reaches your ERP means an order that never gets fulfilled, or an invoice that never gets created.
Broken automation chains
Automation workflows in n8n, Make, or Zapier are only as reliable as the trigger event. One dropped webhook breaks the entire downstream sequence silently.
Zero visibility
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.
Synchronous vs asynchronous webhooks
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:
- → Reliability — the event is stored before dispatch, so it survives any failure in the delivery path
- → Performance — the user-facing request is never blocked by an outbound HTTP call to a slow external API
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 →
What a reliable webhook setup looks like
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:
Persistent queue
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.
Automatic retry with backoff
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.
Background dispatch
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.
Delivery logs
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.
Replay capability
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.
WordPress limitations and workarounds
WordPress was not designed as event infrastructure, and that shows in several ways. Understanding the limitations helps you work around them effectively.
WP-Cron is not a real scheduler
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 execution model
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.
No built-in queue system
WordPress core has no persistent job queue. The common solutions are:
- → Custom database table — full control, requires schema creation and maintenance
- → Action Scheduler — an open-source library (included with WooCommerce) that provides a persistent job queue with retry logic and a UI
- → WordPress Webhook Plugin — purpose-built delivery layer with queue, retry, logs, and replay out of the box
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.
Fire-and-forget vs production-grade delivery
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 WordPress webhook delivery issues
Debugging a fire-and-forget webhook implementation is painful: there is nothing to debug with. A production-grade setup makes debugging straightforward.
Start with delivery logs
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.
Inspect the payload
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.
Test your endpoint independently
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.
Retry manually
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.
Use webhook.site during development
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.
Best practices for WordPress webhooks
Most of these follow directly from the failure modes described above. They're not optional for anything you run in production.
- → Always log every request — HTTP status, response body, and duration on every attempt, success or failure
- → Never rely on a single delivery attempt — assume the first attempt will sometimes fail
- → Use exponential backoff for retries — immediate retries overwhelm already-struggling endpoints
- → Distinguish retryable from permanent failures — 5xx is retryable; 4xx is a configuration problem that needs human attention
- → Store a unique event ID before dispatch — the same ID travels with every retry; receivers use it to deduplicate
- → Validate the response, not just the status code — some endpoints return 200 with an error in the body
- → Keep payloads versioned — add a version field so receivers can handle schema evolution gracefully
- → Monitor queue depth — a growing backlog is an early warning sign of endpoint problems
- → Set up alerts on permanent failures — silent dead-letter queues are the worst of all outcomes
The 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.
Real examples: step-by-step guides
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.
- → Gravity Forms webhook setup — endpoint configuration, field mapping, retry and delivery log
- → Contact Form 7 webhook guide — plugin setup, payload format, making it failure-proof
- → All WordPress webhook integration examples →
- → Async webhooks in WordPress — moving dispatch out of the PHP request cycle
- → WordPress webhook retry and replay system — queue, backoff, and observability
- → Why WP-Cron is not enough for async webhooks — scheduling gaps and fixes
Common questions answered.
Have more questions? Browse the full docs or check the API reference.
- → Gravity Forms webhook example — complete setup with delivery logs and retry
- → Contact Form 7 webhook guide — making CF7 integrations failure-proof
- → Why WordPress webhooks silently fail in production — every failure mode mapped
- → Async webhooks in WordPress — architecture and implementation
- → Why WP-Cron is not enough — the scheduling gap that causes silent failures
- → WordPress webhook retry and replay system — queue design and backoff strategy