WP Webhooks / Blog / Architecture
Article · Architecture

WordPress Webhooks REST API: Retry, Replay & Delivery Logs

Retry failed webhooks, replay any event, and monitor queue health from a REST API — no wp-admin. Full endpoint reference for the WP Webhooks plugin.

10 min 2026-03-19
#wordpress#webhook

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 the X-FSWA-Token header

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

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 delivery pipeline with REST recovery pointsA do_action fires the HooksHandler listener, which dispatches, builds a payload, and enqueues a pending job. The WP-Cron or Action Scheduler runner delivers it: a 2xx logs success, a 5xx or timeout enters the retry queue with backoff until max attempts mark it permanently failed, and any status can be replayed manually.

2xx

5xx / timeout

max attempts

manual

WordPress action fires (do_action)

HooksHandler listener

Dispatcher — normalize args, build payload

QueueService — enqueue (pending)

WP-Cron / Action Scheduler runner

LogService: success

retry queue (exponential backoff)

permanently_failed

replay (any status)

Method & endpointPurpose
GET /logsInspect deliveries of any status
GET /queue/statsQueue depth
GET /healthAggregate overview
POST /logs/{id}/retryRe-send a stored payload
POST /logs/bulk-retryBatch recovery
POST /logs/{id}/replayIntentional resend
POST /webhooks/{id}/toggleEnable / 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.

ScopeHeaderPermitted operations
readX-FSWA-TokenGET /logs, GET /logs/stats, GET /queue/stats, GET /health
operationalX-FSWA-TokenAll read operations, plus POST /logs/{id}/retry, POST /logs/bulk-retry, POST /logs/{id}/replay, POST /webhooks/{id}/toggle
fullX-FSWA-TokenAll 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.

FAQ

Common questions always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

Can I retry failed WordPress webhook deliveries via a REST API? +
Yes. The Flow Systems Webhook Actions plugin exposes POST /wp-json/fswa/v1/logs/{id}/retry, which re-sends a failed delivery using the stored payload without re-triggering the original WordPress action. Bulk retry is also available via POST /logs/bulk-retry with an array of log IDs.
How do I inspect WordPress webhook delivery logs programmatically? +
Use GET /wp-json/fswa/v1/logs?status=error to list failed deliveries, or GET /logs/stats?days=7 for aggregated stats. Filter by webhook_id, trigger_name, or status (pending, success, error, retry, permanently_failed). Requires a token with read scope.
What authentication does the WordPress webhook REST API use? +
The API uses token-based authentication via the X-FSWA-Token header. Tokens are scoped: read for inspection, operational for retry/replay/toggle, full for all operations including configuration changes, and agent — the same write access as full but it can never reveal a webhook's Authorization header or any stored Credentials Vault secret, intended for AI assistants.
Ready

Your next automation is
one sentence away.

$ wp plugin install flowsystems-webhook-actions --activate