Outbound Webhooks
Benecaster can tell another service when something happens to a subscriber or listener, by sending it a signed HTTP POST. Use it to trigger a Zapier or Make automation, keep a CRM or email platform in step, or drive your own integration without polling the REST API.
Everything goes to one URL. The receiving service decides which events it cares about, using the event name or the source field in each request.
Setting It Up
Go to Benecaster → Settings → Webhooks.
- In Endpoint URL, enter the address the other service gave you. It must start with
https://(orhttp://). - Tick Send events to this URL and click Save webhook settings. Switching sending on for the first time creates a signing secret.
- Under Signing secret, click Generate secret (or Rotate secret if one is already set). The secret appears once, with a Copy button. Copy it into the receiving service now: it is never shown again, and reloading the page hides it for good.
- Under Test, click Send test event. Benecaster sends a
webhook.testevent straight away and shows what came back: Delivered — the endpoint answered 200., or Not delivered: followed by the error.
Rotating the secret creates a new one and shows it once. Requests signed with the old secret stop verifying straight away, so update the receiving service at the same time.
To pause delivery without losing the URL, untick Send events to this URL and save.
Events sent lists every event that goes to the URL. Benecaster’s own are under a Benecaster heading, and each add-on that sends events has its own heading with its name. There is no switch per event or per add-on: filter in the receiving service.
Recent deliveries shows the last 50 deliveries, newest first, with the event, Sent by (Benecaster or the add-on’s name), the status (Queued, Sending, Retrying, Delivered or Failed), the number of attempts, the response or error, and when it was queued. Deliveries are kept for 30 days. The body that was sent is never shown, because it holds subscribers’ email addresses.
Staging sites
A staging site sends no events at all, and the screen says so. A staging copy of your live site must not send your real subscribers to your production integrations. Send test event still works there, so you can check a receiving URL.
How Delivery Works
Events are queued and sent in the background by WordPress’s scheduled tasks (WP-Cron), never in the middle of a checkout or a feed request. Each delivery is tried up to three times: straight away, then 5 minutes later, then 30 minutes after that. Any 2xx response counts as delivered. Anything else, or no answer in time, is retried, and after the third failure the delivery is marked Failed.
De-duplicate on the delivery id. A slow 2xx can be retried, so the same event can arrive more than once. Every retry of one event carries the same X-Benecaster-Delivery header and the same delivery_id in the body. Store it, and ignore a delivery id you have already handled.
WP-Cron runs when someone visits your site. On a quiet site, events can wait until the next visit. For prompt delivery, set up a real cron job for WordPress (most hosts offer one).
When deliveries fail
When the three most recently finished deliveries have all failed, each after its three attempts, a warning appears in Benecaster’s Notifications bell: Your outbound webhook is failing…, naming the latest error, with an Open webhook settings button. It clears by itself as soon as an event is delivered. A successful Send test event clears it straight away, and so does switching sending off. It is never shown on a staging site, which sends nothing.
That notice is the only alert. Nothing is emailed, so if you rely on a webhook, check the bell or Recent deliveries now and then.
Verifying Signatures
Every request carries an X-Benecaster-Signature header:
X-Benecaster-Signature: sha256=<hex-encoded HMAC-SHA256>
Compute the HMAC-SHA256 of the raw request body with your signing secret and compare it with the header, using a constant-time comparison. Do this before trusting anything in the request, and reject a request whose signature is missing or wrong.
PHP example:
function verify_benecaster_webhook( string $raw_body, string $signature_header, string $secret ): bool {
$expected = 'sha256=' . hash_hmac( 'sha256', $raw_body, $secret );
return hash_equals( $expected, $signature_header );
}
Node.js example:
const crypto = require('crypto');
function verifyBenecasterWebhook(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}
Request Format
Each request is a POST with a JSON body and these headers:
| Header | Holds |
|---|---|
X-Benecaster-Event |
The event name |
X-Benecaster-Delivery |
The delivery id, the same on every retry of one event |
X-Benecaster-Signature |
sha256= and the HMAC of the raw body |
Every body carries these keys:
| Key | Type | Description |
|---|---|---|
event |
string | The event name — see Event Catalog |
delivery_id |
string | The same value as the X-Benecaster-Delivery header |
source |
string | Who sent the event: core for Benecaster’s own events, or the add-on’s slug (for example podcast-workflow) |
schema_version |
integer | The version of this event’s body shape. 1 for every Benecaster event. It goes up only when a key is removed or changes meaning, never when one is added, so ignore keys you do not recognise |
occurred_at |
string | When the event happened (not when it was sent), ISO 8601, UTC |
Benecaster’s own events also carry show_id, user_id and email (the subscriber’s address), plus the keys listed for each event below:
{
"event": "subscription.tier_moved",
"delivery_id": "6f1c2b9e-3a47-4f0e-9a51-2d8c7e4b1f03",
"source": "core",
"schema_version": 1,
"show_id": 42,
"user_id": 1007,
"email": "subscriber@example.com",
"old_tier_slug": "silver",
"tier_slug": "gold",
"direction": "upgrade",
"occurred_at": "2026-10-19T14:32:00+00:00"
}
A raw feed token is never sent. Token events carry token_id, the record’s id, not the token itself.
Event Catalog
| Event | When it is sent | Keys besides show_id, user_id, email |
|---|---|---|
subscription.activated |
A membership starts | tier_slug, activation_source |
subscription.cancelled |
A membership is cancelled | — |
subscription.tier_changed |
A built-in membership subscriber’s tier changes | old_tier_slug, tier_slug |
subscription.tier_moved |
A subscriber’s tier changes, whatever reported it, membership plugins included | old_tier_slug, tier_slug, direction (upgrade, downgrade, or null when the tiers cannot be ranked) |
subscription.payment_failed |
A renewal payment fails | tier_slug |
token.generated |
A feed URL is created for a subscriber | tier_slug, token_id |
token.reset |
A subscriber’s feed URL is reset | token_id |
token.revoked |
A feed URL is switched off | token_id, reason, token_type, token_type_base |
token.reactivated |
A switched-off feed URL comes back, usually when someone resubscribes. The URL is unchanged | token_id, tier_slug |
subscriber.feed_first_accessed |
A podcast app polls the subscriber’s feed for the first time | tier_slug |
subscriber.feed_inactive |
The subscriber’s app has not polled for longer than the show’s inactivity threshold | tier_slug, days_inactive |
subscriber.feed_reactivated |
An inactive subscriber’s app polls again | tier_slug, days_inactive |
subscriber.never_accessed |
A feed URL issued past its grace window has never been polled | tier_slug, days_since_created |
marketing_consent.changed |
A marketing-consent answer is recorded or changes, or an unconfirmed answer becomes confirmed | granted, verified, consent_source (subscribe, follower_signup or donation) |
webhook.test |
You click Send test event | site_url only: no show_id, user_id or email |
To hear every tier change, listen for subscription.tier_moved. subscription.tier_changed covers only the built-in membership.
activation_source says how a membership started: new, resubscribe, resumed or migration. See Privacy and Data Handling for what a confirmed consent answer means.
Membership plugins
A plan change in a membership plugin reaches Benecaster as the subscriber starting the new plan, so it sends subscription.activated and subscription.tier_moved, not a cancellation:
- Restrict Content Pro: a plan change sends
subscription.tier_movedandsubscription.activated. Nosubscription.cancelled,token.revokedortoken.reactivatedis sent for it. - WooCommerce Subscriptions: a plan switch sends
subscription.tier_movedandsubscription.activated. - MemberPress:
subscription.activated(andtoken.reactivatedfor a returning subscriber) is sent when MemberPress records the payment — the first completed transaction, or the start of a free trial — not when the checkout form is submitted. An abandoned or declined checkout sends nothing.
Add-on Events
An add-on can send its own events to the same URL, signed with the same secret and delivered through the same queue, retries and staging rule. Its events appear under its own name in Events sent and Sent by, with its slug in source. Event names take the form {add-on}.{noun}.{verb}, for example podcast-workflow.guest.booked.
An add-on sends events only for shows it is switched on for. A delivery already queued is still sent, retries included, if the add-on is switched off or its license lapses before it goes out; nothing new is queued after that.
An add-on event carries event, delivery_id, source, schema_version, show_id and occurred_at; user_id and email only when the event is about a person; then the add-on’s own keys.
Sending Data Back
The other direction, a service telling an add-on that something happened, goes through Incoming actions, further down the same Settings → Webhooks screen. An add-on event usually carries the add-on’s own record id (for example guest_id), so the service can send it back with its answer. See Connecting Zapier, Make or a CRM, and for add-on developers the recipe Let Zapier, Make or a CRM call your add-on.
REST Endpoints
The screen is built on these routes. All need manage_options and a valid wp_rest nonce.
| Route | Does |
|---|---|
GET /benecaster/v1/webhook-settings |
Returns url, enabled, secret_set, staging (true when events are not being sent), events (Benecaster’s event names) and addon_events (each add-on’s events, grouped by add-on). Never the secret |
POST /benecaster/v1/webhook-settings |
Saves url and enabled. A secret is created the first time sending is switched on |
POST /benecaster/v1/webhook-settings/rotate |
Creates a new secret and returns it as { "secret": "…" }. The only response that ever contains it |
POST /benecaster/v1/webhook-settings/test |
Sends webhook.test at once, outside the queue and on a staging site too, and returns { ok, response_code, error } |
GET /benecaster/v1/webhook-settings/deliveries |
The last 50 deliveries: id, event, source, source_name, status (pending, sending, sent or failed), attempts, response_code, last_error, created_at, sent_at (UTC). Never the body |
Developer Filter
benecaster_webhook_payload filters each body when the event is queued, so the stored body is exactly what goes out on every attempt. Add keys, remove ones you would rather not send, or return null to send nothing for that event. It does not run for webhook.test. Add-on events pass through it too: check $payload['source'] to tell them apart.
add_filter( 'benecaster_webhook_payload', function( array $payload, string $event ): ?array {
// Send nothing for feed URL resets.
if ( 'token.reset' === $event ) {
return null;
}
// Add a field the receiving service needs.
$payload['site_id'] = get_current_blog_id();
return $payload;
}, 10, 2 );
See the benecaster_webhook_payload reference.
See Also
Need this built rather than just documented? See our services →