TL;DR
- The Webhook Actions REST API lets you inspect delivery logs, retry failed events, replay any stored event, and monitor queue health without touching wp-admin
- Bulk retry and bulk replay endpoints let you recover from outages or resend backlogs programmatically — one API call, multiple events
- All endpoints live under
/wp-json/fswa/v1/and authenticate via theX-FSWA-Tokenheader
/ The Problem
Why treat webhooks as One-Shot HTTP?
The default WordPress webhook is a single HTTP call made inline during the PHP request. It fires once. If the receiving endpoint is down, rate-limited, or returns a 500, the delivery attempt produces nothing — no record, no alert, no rescheduled retry.
The event is not just undelivered. It is gone. There is no delivery log to inspect, no failure to surface, no mechanism to recover. Integration drift accumulates silently: your CRM, ERP, or automation platform diverges from WordPress reality while both systems report nominal health.
Even with retry and replay infrastructure in place, operating that infrastructure required logging into wp-admin. Useful for developers in the browser — not useful for monitoring systems, CI pipelines, or automation agents that need to query or act on delivery state without human intervention. AI agents now get the same programmatic surface too, via the WordPress Abilities API and MCP.
The failure modes that make this the default are covered in why WordPress webhooks silently fail in production — the structural problems that make the default model unreliable at scale.
/ The Idea
How do you move from triggers to Event Lifecycle Management?
The shift worth making is conceptual: stop thinking about webhooks as triggers and start treating them as an event pipeline with a lifecycle — persistence, dispatch, observation, recovery.
That lifecycle already existed inside the plugin. Every event is stored before dispatch. Failures enter a retry queue with exponential backoff. Successful deliveries remain available for manual replay. The full attempt history — timestamps, HTTP status codes, response bodies — is recorded per event.
The API was always there, powering the wp-admin UI. What changed is that it is now exposed. The same operations available in the WordPress admin panel — inspect logs, retry failures, replay events, check queue health, toggle webhooks — are now reachable via authenticated HTTP from any system that can make a web request.
This turns WordPress webhook infrastructure into something operable: scriptable, monitorable, automatable, and auditable without a browser.
/ What You Can Do
API Capabilities
Inspect Delivery Logs
Query the full history of every webhook delivery attempt. Filter by status (pending, success, error, retry, permanently_failed), by specific webhook ID, or by trigger name. Each log record includes the HTTP response code, response body, timestamp, attempt number, and the original payload that was sent.
Delivery stats are available separately — aggregate counts and success rates over a configurable time window — for dashboards and health checks.
Retry Failed Events
Trigger a retry for any failed delivery log via the API. The retry uses the stored payload verbatim — the original WordPress action is not re-fired. This means you can recover a failed WooCommerce order event without creating a duplicate order, re-send a failed form submission without re-running the form hook, or recover any dead-letter event without side effects in WordPress itself.
Bulk retry accepts an array of log IDs, useful for recovering a batch of failures that occurred during a downstream outage.
Replay Events
Replay resends a previously successful delivery. Where retry is for failures, replay is for intentional resend — debugging a receiving system bug, rebuilding state after a migration, or testing a fix to an endpoint that was previously processing payloads incorrectly.
The X-Event-ID header carries the same UUID as the original event, giving receiving endpoints the information they need to deduplicate if needed.
Monitor Queue Health
GET /queue/stats returns pending, processing, completed, and failed counts. GET /health returns an aggregate overview — queue health, velocity metrics, recent failure rates. Both endpoints are lightweight enough to poll from a monitoring system or include in a dashboard.
Enable / Disable Webhooks Programmatically
Toggle any webhook on or off via the API. Useful for deployment workflows (disable webhooks during a database migration, re-enable after), for incident response (disable a misbehaving webhook without touching wp-admin), or for test environments that should not fire to production endpoints.
Creating webhooks, updating endpoint URLs and triggers, listing and deleting them is the other half of the API, covered in the next section. It needs a full scope token; everything in the operations examples further down works with read or operational.
/ Create
Create a Webhook via REST API
Base URL: https://your-site.com/wp-json/fswa/v1
Auth header: X-FSWA-Token: <token> — requires full scope
A webhook needs two things: a name and an endpoint URL. Everything else is optional — triggers (which WordPress actions fire this webhook), auth header (sent with each delivery), and whether it starts enabled.
curl — create a webhook
curl -X POST "https://your-site.com/wp-json/fswa/v1/webhooks" -H "X-FSWA-Token: your-full-scope-token" -H "Content-Type: application/json" -d '{ "name": "CF7 → n8n", "endpoint_url": "https://n8n.yourdomain.com/webhook/abc123", "triggers": ["wpcf7_mail_sent"], "is_enabled": true }'
response — JSON
{
"id": 7,
"name": "CF7 → n8n",
"endpoint_url": "https://n8n.yourdomain.com/webhook/abc123",
"auth_header": null,
"auth_credential_id": null,
"is_enabled": true,
"triggers": ["wpcf7_mail_sent"],
"created_at": "2026-03-25 11:04:38",
"updated_at": "2026-03-25 11:04:38"
} id is the webhook identifier used in all subsequent operations. triggers maps to WordPress action hook names — use GET /wp-json/fswa/v1/triggers to browse all available hooks grouped by category (WooCommerce, Contact Form 7, WordPress core, etc.).
With authorization — the Credentials Vault
If the receiving endpoint requires authentication, the preferred way is to store the secret once in the plugin's Credentials Vault and reference it by id via auth_credential_id. It takes precedence over the legacy plaintext auth_header field, and setting it to null clears it. Vault secrets are encrypted at rest and write-only over the API — the value is sent as the Authorization header (or a custom header) with every delivery, but can never be read back, not even by the agent-scope tokens used by AI assistants.
curl — store the secret once in the Credentials Vault
curl -X POST "https://your-site.com/wp-json/fswa/v1/credentials" -H "X-FSWA-Token: your-full-scope-token" -H "Content-Type: application/json" -d '{ "name": "HubSpot PAT", "type": "bearer", "secret": "hs_live_secret_abc123" }' # → { "id": 3, "name": "HubSpot PAT", "hint": "…c123" } — the secret is never returned
curl — webhook referencing the vault credential
curl -X POST "https://your-site.com/wp-json/fswa/v1/webhooks" -H "X-FSWA-Token: your-full-scope-token" -H "Content-Type: application/json" -d '{ "name": "WooCommerce → HubSpot", "endpoint_url": "https://hooks.example.com/crm/orders", "auth_credential_id": 3, "triggers": ["woocommerce_order_status_completed"], "is_enabled": true }'
JavaScript — fetch
JavaScript — create webhook
const response = await fetch( 'https://your-site.com/wp-json/fswa/v1/webhooks', { method: 'POST', headers: { 'X-FSWA-Token': 'your-full-scope-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'CF7 → n8n', endpoint_url: 'https://n8n.yourdomain.com/webhook/abc123', triggers: ['wpcf7_mail_sent'], is_enabled: true }) } ); const webhook = await response.json(); // webhook.id — use this for all subsequent operations
/ List & Inspect
List and Inspect Webhooks
Listing webhooks requires only a read scope token — you don't need write access to audit what's configured. This is the right token scope for monitoring scripts, dashboards, and read-only tooling.
curl — list all webhooks
curl -X GET "https://your-site.com/wp-json/fswa/v1/webhooks" -H "X-FSWA-Token: your-read-scope-token" # Add ?only_enabled=true to return only active webhooks
response — JSON (array)
[
{
"id": 5,
"name": "Order sync — HubSpot",
"endpoint_url": "https://hooks.example.com/crm/orders",
"is_enabled": true,
"triggers": ["woocommerce_order_status_completed"],
"created_at": "2026-02-10 08:30:00",
"updated_at": "2026-03-24 14:15:22"
},
{
"id": 7,
"name": "CF7 → n8n",
"endpoint_url": "https://n8n.yourdomain.com/webhook/abc123",
"is_enabled": true,
"triggers": ["wpcf7_mail_sent"],
"created_at": "2026-03-25 11:04:38",
"updated_at": "2026-03-25 11:04:38"
}
] Get a single webhook
curl — get webhook by ID
curl -X GET "https://your-site.com/wp-json/fswa/v1/webhooks/7" -H "X-FSWA-Token: your-read-scope-token"
/ Update & Control
Update and Control Webhooks
Update a webhook
PATCH /webhooks/{id} accepts any subset of fields — you only send what you want to change. Requires full scope.
curl — change endpoint URL
curl -X PATCH "https://your-site.com/wp-json/fswa/v1/webhooks/7" -H "X-FSWA-Token: your-full-scope-token" -H "Content-Type: application/json" -d '{ "endpoint_url": "https://n8n.yourdomain.com/webhook/production-xyz" }'
curl — add a trigger to an existing webhook
curl -X PATCH "https://your-site.com/wp-json/fswa/v1/webhooks/5" -H "X-FSWA-Token: your-full-scope-token" -H "Content-Type: application/json" -d '{ "triggers": ["woocommerce_order_status_completed", "woocommerce_order_status_refunded"] }'
Delete a webhook
Remove a webhook entirely. Requires full scope. This cannot be undone — the webhook and its configuration are deleted permanently.
curl — delete webhook
curl -X DELETE "https://your-site.com/wp-json/fswa/v1/webhooks/7" -H "X-FSWA-Token: your-full-scope-token"
response — JSON
{ "deleted": true, "id": 7 } Provisioning from a deployment pipeline
During a deployment pipeline, create environment-specific webhooks automatically. Point staging to a test endpoint, production to the live one. Disable all webhooks before a database migration, re-enable after it completes. No one logs into wp-admin during a deployment window.
bash — CI/CD deployment script
SITE="https://your-site.com/wp-json/fswa/v1" TOKEN="${FSWA_FULL_TOKEN}" # Disable all webhooks before migration WEBHOOK_IDS=$(curl -s "${SITE}/webhooks?only_enabled=true" -H "X-FSWA-Token: ${TOKEN}" | jq '[.[].id]') for id in $(echo "${WEBHOOK_IDS}" | jq '.[]'); do curl -s -X POST "${SITE}/webhooks/${id}/toggle" -H "X-FSWA-Token: ${TOKEN}" > /dev/null done # ... run migration ... # Re-enable after migration completes for id in $(echo "${WEBHOOK_IDS}" | jq '.[]'); do curl -s -X POST "${SITE}/webhooks/${id}/toggle" -H "X-FSWA-Token: ${TOKEN}" > /dev/null done
/ Code Examples
API Examples
Base URL: https://your-site.com/wp-json/fswa/v1
Auth header: X-FSWA-Token: <token>
1. List Failed Delivery Logs
Filter by status, webhook ID, or trigger name. Requires read scope.
curl
curl -X GET \ "https://your-site.com/wp-json/fswa/v1/logs?status=error" \ -H "X-FSWA-Token: your-token"
JavaScript — fetch
const response = await fetch( 'https://your-site.com/wp-json/fswa/v1/logs?status=error', { headers: { 'X-FSWA-Token': 'your-token' } } ); const logs = await response.json(); // Available status values: pending | success | error | retry | permanently_failed // Optional filters: ?webhook_id=5&trigger_name=woocommerce_order_created
response — JSON
[
{
"id": 42,
"trigger_name": "woocommerce_order_created",
"status": "error",
"http_code": "503",
"response_body": "Service Unavailable",
"error_message": "HTTP 503: Service Unavailable",
"duration_ms": "2341",
"created_at": "2026-03-19 09:14:22",
"event_uuid": "d4e1f8a2-3c9b-4f1e-a7d2-8b5c1e6f9a3d",
"attempt_history": [
{ "attempt": 1, "http_code": 503, "status": "error", "attempted_at": "2026-03-19T09:14:22Z" },
{ "attempt": 2, "http_code": 503, "status": "error", "attempted_at": "2026-03-19T09:44:22Z" }
]
}
] response_body surfaces the exact message returned by the downstream endpoint — the fastest way to identify whether the failure was a 503 outage, a 401 auth issue, or a 422 schema mismatch. attempt_history shows the full retry trail per event, including timestamps and per-attempt HTTP codes.
2. Get Delivery Stats
Aggregate success/failure counts over a time window. Requires read scope.
curl
curl -X GET \ "https://your-site.com/wp-json/fswa/v1/logs/stats?days=7" \ -H "X-FSWA-Token: your-token"
JavaScript — fetch
const stats = await fetch( 'https://your-site.com/wp-json/fswa/v1/logs/stats?days=7', { headers: { 'X-FSWA-Token': 'your-token' } } ).then(r => r.json()); // Optional: ?webhook_id=5 to scope to one webhook
response — JSON
{
"success": 38,
"error": 0,
"retry": 2,
"permanently_failed": 3,
"pending": 0,
"avg_duration_ms": 234,
"http_2xx": 38,
"http_4xx": 1,
"http_5xx": 4,
"total": 43
} permanently_failed is the number to alert on. avg_duration_ms tracks endpoint latency over time — a gradual increase signals a degrading downstream service before it starts hard-failing.
3. Retry a Failed Event
Re-sends the stored payload without re-triggering the WordPress action. Requires operational scope.
curl
curl -X POST \ "https://your-site.com/wp-json/fswa/v1/logs/42/retry" \ -H "X-FSWA-Token: your-token"
JavaScript — fetch
const logId = 42; const result = await fetch( `https://your-site.com/wp-json/fswa/v1/logs/${logId}/retry`, { method: 'POST', headers: { 'X-FSWA-Token': 'your-token' } } ).then(r => r.json());
response — JSON
{ "success": true, "job_id": 74 } job_id is the queue entry for this retry. Use it to correlate with GET /queue/stats output when debugging whether the job was picked up by the dispatcher.
4. Bulk Retry Multiple Failed Events
Recover a batch of failures at once. Requires operational scope.
curl
curl -X POST \ "https://your-site.com/wp-json/fswa/v1/logs/bulk-retry" \ -H "X-FSWA-Token: your-token" \ -H "Content-Type: application/json" \ -d '{"ids": [42, 43, 44]}'
JavaScript — fetch
await fetch( 'https://your-site.com/wp-json/fswa/v1/logs/bulk-retry', { method: 'POST', headers: { 'X-FSWA-Token': 'your-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: [42, 43, 44] }) } );
response — JSON
{ "retried": 3, "skipped": 0 } skipped counts IDs that were not in a retriable state — already pending, processing, or succeeded. Non-zero means your ID set included events that didn't need recovery. Check this in automated scripts to detect stale IDs.
5. Replay a Successful Event
Resend a previously delivered event. Carries the original X-Event-ID for deduplication. Requires operational scope.
curl
curl -X POST \ "https://your-site.com/wp-json/fswa/v1/logs/17/replay" \ -H "X-FSWA-Token: your-token"
JavaScript — fetch
const logId = 17; await fetch( `https://your-site.com/wp-json/fswa/v1/logs/${logId}/replay`, { method: 'POST', headers: { 'X-FSWA-Token': 'your-token' } } );
6. Queue Stats & Health
Poll queue depth and system health from a monitoring system or uptime dashboard. Requires read scope.
curl — queue stats
curl -X GET \ "https://your-site.com/wp-json/fswa/v1/queue/stats" \ -H "X-FSWA-Token: your-token" # Returns: pending, processing, completed, failed counts
curl — system health overview
curl -X GET \ "https://your-site.com/wp-json/fswa/v1/health" \ -H "X-FSWA-Token: your-token" # Returns: aggregate stats, queue health, velocity metrics
response — JSON (/health)
{
"success_rate": 89,
"webhooks": { "total": 4, "active": 4 },
"logs": {
"total": 43,
"success": 38,
"permanently_failed": 3,
"retry": 2
},
"queue": {
"pending": 2,
"processing": 0,
"permanently_failed": 3,
"due_now": 2
},
"velocity": {
"last_hour": 12,
"last_day": 43,
"avg_duration_ms": 234
},
"observability": {
"avg_attempts_per_event": 1.4,
"queue_stuck": false,
"wp_cron_only": false
}
} queue.due_now is the most actionable field for alerting — non-zero means jobs are overdue and WP-Cron may not be running. observability.queue_stuck flags this explicitly. velocity.last_hour and avg_duration_ms feed directly into dashboard panels without additional aggregation.
7. Toggle a Webhook On / Off
Disable during deployments or incidents. Re-enable without touching wp-admin. Requires operational scope.
curl
curl -X POST \ "https://your-site.com/wp-json/fswa/v1/webhooks/3/toggle" \ -H "X-FSWA-Token: your-token" # No request body needed. Toggles current state.
JavaScript — fetch
const webhookId = 3; await fetch( `https://your-site.com/wp-json/fswa/v1/webhooks/${webhookId}/toggle`, { method: 'POST', headers: { 'X-FSWA-Token': 'your-token' } } );
response — JSON
{
"id": 3,
"name": "Order sync — HubSpot",
"endpoint_url": "https://hooks.example.com/wp/order",
"is_enabled": false,
"updated_at": "2026-03-19 14:22:11"
} Returns the full updated webhook state. Always check is_enabled in your script rather than assuming the toggle succeeded — this is the confirmation.
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.
/ Real-World Example
WooCommerce → HubSpot: Recovering from an Outage
A WooCommerce store sends order events to HubSpot via webhook. HubSpot has a 9-minute API outage. Forty-three order events fail to deliver during that window.
Before the API
The team gets a Slack alert from their monitoring system: webhook failure rate spiked. Someone logs into wp-admin, navigates to the delivery log, confirms the failures, then starts clicking "retry" individually. For 43 events. Then checks back manually to confirm each one succeeded.
The same operations in the Symfony CLI wrapper someone wrote six months ago:
bash — before
# Manual recovery script — required SSH access + Symfony CLI php bin/console webhooks:list-failed --format=json | \ jq '.[] | .id' | \ xargs -I{} php bin/console webhooks:retry {} # Required: SSH access, PHP CLI, Symfony console, custom commands # Risk: fires during business hours, no rate limiting, no audit trail
After the API
HubSpot's status page shows all-clear. The monitoring system calls a recovery script automatically — no human in the loop:
bash — after
# Automated recovery — triggered by monitoring system SITE="https://store.example.com/wp-json/fswa/v1" TOKEN="${FSWA_TOKEN}" # Get all permanently failed log IDs IDS=$(curl -s \ "${SITE}/logs?status=permanently_failed&webhook_id=7" \ -H "X-FSWA-Token: ${TOKEN}" \ | jq '[.[].id]') # Bulk retry in one request curl -s -X POST \ "${SITE}/logs/bulk-retry" \ -H "X-FSWA-Token: ${TOKEN}" \ -H "Content-Type: application/json" \ -d "{"ids": ${IDS}}" # No SSH. No wp-admin. No manual clicks. Auditable. Repeatable.
The same pattern works for more complex scenarios: disable a webhook before a database migration, replay a specific order event to test a receiving-side bug fix, query queue depth every 60 seconds from a Grafana datasource. All from outside WordPress, with no wp-admin dependency.
/ Architecture Diagram
Event Lifecycle
Every webhook event follows this path. The REST API provides programmatic access to every node in the lifecycle — logs, retry queue, replay, health.
| Method & endpoint | Purpose |
|---|---|
GET /logs | Inspect deliveries of any status |
GET /queue/stats | Queue depth |
GET /health | Aggregate overview |
POST /logs/{id}/retry | Re-send a stored payload |
POST /logs/bulk-retry | Batch recovery |
POST /logs/{id}/replay | Intentional resend |
POST /webhooks/{id}/toggle | Enable / disable a webhook |
The API surfaces the full delivery history at every node. GET /logs queries any status. GET /queue/stats exposes queue depth. GET /health aggregates across the whole pipeline. Retry and replay endpoints operate directly on stored payloads — no WordPress action re-fires, no side effects.
See the async webhook architecture post for how dispatch is decoupled from the PHP request cycle.
/ Authentication
Token Scopes
Tokens are issued per-site from the plugin settings panel. Each token carries one of three scopes. Use the minimum scope required for the operation.
| Scope | Header | Permitted operations |
|---|---|---|
read | X-FSWA-Token | GET /logs, GET /logs/stats, GET /queue/stats, GET /health |
operational | X-FSWA-Token | All read operations, plus POST /logs/{id}/retry, POST /logs/bulk-retry, POST /logs/{id}/replay, POST /webhooks/{id}/toggle |
full | X-FSWA-Token | All operational operations, plus the webhook configuration endpoints |
Monitoring systems and dashboards should use read tokens. Recovery scripts and CI pipelines should use operational. No token should ever carry more scope than its use case requires.
Tokens are revocable independently. If an operational token is exposed in a script or log, revoke it from the plugin settings without affecting other tokens or the site's webhook configuration.
/ AI-Driven Workflows
An Unexpected Side Effect
When the API was first exposed internally, the expected use cases were monitoring scripts and CI pipelines. The less expected use case emerged almost immediately: AI coding agents.
Claude Code, Cursor, and similar agents can invoke the API directly when given a token with read or operational scope. An agent debugging an integration failure can inspect the delivery log, examine the response body, identify the failure pattern, and retry the affected events — without a developer opening a browser or logging into wp-admin.
This is a meaningful shift in how webhook infrastructure gets operated. An on-call agent can run a morning health check: GET /health, surface any permanently failed events, identify the affected webhooks, check if the downstream service is back up (via a separate status check), and issue a bulk-retry if appropriate. The human reviews the action log, not the raw data.
The prerequisite for this to work is exactly what the API provides: a machine-readable interface to delivery state, with idempotent operations that have clear, bounded effects.
/ When This Approach Makes Sense
Operational Fit
The REST API is the right approach when:
- your team operates on-call and needs a recovery path that does not require wp-admin access or SSH to the server during an incident.
- you run CI/CD pipelines that deploy to environments with webhooks configured — disable before deploy, re-enable after, verify queue is draining normally.
- you have a monitoring system that should alert on backlog depth, failure rate, or permanently failed events — metrics the API surfaces directly.
- you manage multiple WordPress sites and want a single control plane for webhook health across all of them, without logging into each site individually.
- your receiving systems occasionally have outages and you need a reliable, scriptable recovery path — especially for batch recovery of many events at once.
- you are building AI-assisted operations tooling and want agents to be able to observe and act on webhook delivery state as part of a broader automation.
If you are running a simple WordPress site with one or two webhooks and low event volume, the wp-admin UI is probably sufficient. The API adds value when the operational complexity of managing webhook delivery outgrows what a browser-based interface can handle efficiently.