/ Article — REST API

WordPress Webhooks REST API:
Retry, Replay and Monitor Events Programmatically

Webhooks fire once and disappear. If the endpoint fails, the event is gone — no log, no retry, no way to know what happened. What if your webhook infrastructure was as operable as any other API?

The plugin's REST API lets you:

/ The Problem

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.

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

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.

If you need the full webhook provisioning API — creating new webhooks, updating endpoint URLs and triggers, listing all registered webhooks, or deleting them — that is covered in the companion article: Create and Manage WordPress Webhooks Programmatically.

/ 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.

/ 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.

Webhook event lifecycle diagram

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: <token>     GET /logs
                                          GET /logs/stats
                                          GET /queue/stats
                                          GET /health

operational    X-FSWA-Token: <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: <token>     All operational operations, plus:
                                          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:

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.

/ Learn More

REST API Documentation & Plugin

Flow Systems Webhook Actions includes the full REST API described in this article. Every endpoint, authentication model, and response format is documented in the REST API reference. The plugin is free and open source, distributed via WordPress.org. See the plugin landing page for the full feature overview.

/ FAQ

Common questions

Yes. POST /wp-json/fswa/v1/logs/{id}/retry re-sends a failed delivery using the stored payload without re-triggering the original WordPress action — so retrying a failed WooCommerce order event won't create a duplicate order.

Bulk retry is also available via POST /logs/bulk-retry with an array of log IDs, which is useful for recovering a batch of failures from a downstream outage in a single request. Both endpoints require a token with operational scope.
Use GET /wp-json/fswa/v1/logs?status=error to list failed deliveries, or GET /logs/stats?days=7 for aggregated counts. You can filter by webhook_id, trigger_name, or status (pending, success, error, retry, permanently_failed).

Each log record includes the HTTP response code, the full response body, timestamp, attempt number, and the original payload. This is enough to diagnose whether a failure was a timeout, a 4xx auth issue, or a transient 5xx — without logging into wp-admin. Requires a token with read scope.
The API uses token-based authentication via the X-FSWA-Token header. Tokens are issued per-site from the plugin settings panel and carry one of three scopes:

read — delivery log inspection, stats, queue depth, health endpoint.
operational — everything in read, plus retry, bulk retry, replay, and webhook toggle.
full — everything in operational, plus webhook configuration endpoints.

Tokens are independently revocable. Use the minimum scope required for each use case — monitoring scripts should use read, recovery scripts operational.