Webhooks push JSON to any endpoint the moment something happens on your site — a form is submitted, content changes, a CRM contact is captured, an order is paid. Paste a Zapier, Make, or n8n catch-hook URL and your site becomes a trigger for thousands of downstream apps; point it at your own endpoint and the deliveries are HMAC-signed so you can verify every request. No code and no API tokens required.
What it does
- Event-driven JSON POSTs to any HTTPS endpoint you add, the instant the subscribed event fires on the site.
- Works with Zapier, Make, n8n, and home-grown automation: catch-hook style receivers work with zero setup; your own endpoints get an
X-Webprocms-Signatureheader to verify. - Per-endpoint event subscriptions: each webhook picks exactly the events it cares about, or
*for everything. - Send test: a one-click button fires a representative sample payload synchronously — so Zapier's "Test trigger" step has a request waiting the moment you switch tabs, with every field available for mapping. Sample keys mirror real deliveries exactly.
- Signed deliveries: every request carries
X-Webprocms-Signature: sha256=<hex>— an HMAC-SHA256 of the raw request body using the webhook's signing secret (shown once at creation). Catch-hook receivers can simply ignore it. - Retried and self-healing: non-2xx responses and transport errors retry up to 4 times with increasing backoff (1 min → 5 min → 30 min); an endpoint that fails 20 deliveries in a row is disabled automatically, and re-enabling it resets the failure budget.
- Observable: the endpoint list shows each webhook's status and last delivery; a per-webhook log keeps the 50 most recent deliveries with response codes (the dashboard shows the latest 10).
Turn it on under Settings → Features → Webhooks (off by default). Manage endpoints under Settings → Webhooks (admins only).
Events
| Event | Fires when |
|---|---|
content_item.created / content_item.updated / content_item.deleted |
A content item is created / updated / deleted. The occurrences a repeating date materializes are copies rebuilt on every save of their item, so they never fire events of their own — the item's own events stand for the series |
form.submitted |
A (non-spam) form submission is received — public form or API. A form with a payment field fires once its payment completes (a bank payment: when it clears), never for an abandoned checkout |
crm.contact.created / crm.contact.updated |
A CRM contact is captured or changed |
subscriber.created / subscriber.unsubscribed |
A marketing subscriber signs up / unsubscribes (someone who never subscribed using the unsubscribe link of a shop, loyalty or CRM email fires both: their row is created already unsubscribed) |
prospect.slides_requested |
A prospect's slideshow pitch is handed to a Claude agent to build ("Have Claude build the slides") — once per request, never again while it waits. The payload carries the business, the request time and the brief_url an agent reads from the API (Prospecting) |
automotive.lead.created |
An automotive lead comes in (any lead form) — once per lead, after its arrival translation, from the lead's forward task. The payload carries the shopper's words (message.original), the team-language translation or null with message.translated: false, the call/text consent record, the store and vehicle, and the same ADF/XML document the dealer's CRM mailbox gets (adf) (Automotive) |
order.created / order.paid / order.status_changed |
A shop order is created / becomes paid / changes status |
project.completed |
A tracked project is marked complete |
job.created / job.completed |
A Jobs & Dispatch job is made (by hand, or from a booking or accepted quote) / is finished — once per change of status; a job a race loser made and rolled back is never announced. The payload carries the client, the service address and the customer's job page link (service_url) (Jobs & Dispatch) |
visit.scheduled |
A job visit is put on the schedule or moved to a new time (a notes-only save or a change of techs alone doesn't fire it). The payload carries the visit's times, its techs and its job |
booking.created / booking.cancelled / booking.completed |
An appointment is confirmed / cancelled / completed — a paid booking once, from the webhook or the return page, whichever lands first (a bank payment: when it clears; one that bounces never fires booking.cancelled, as it never confirmed) |
restaurant_order.placed |
A restaurant order is placed (pay-at-store immediately, online orders on capture — once) |
ticket_order.paid |
An event ticket order completes payment — once, from the webhook or the return page, whichever lands first (a bank payment: when it clears) |
donation.received |
A donation is paid, including recurring renewals |
invoice.paid |
A client invoice becomes paid |
ticket.created / ticket.closed |
A (non-spam) support ticket is opened / closed |
review.received |
A review lands from any source (synced or on-site form) |
directory_listing.paid |
A directory listing term or featured upgrade is paid for |
member.created / member.cancelled |
A member signs up / cancels |
Events fire regardless of how the change happened — dashboard edit, public form post, checkout, or API call.
Events only fire for add-ons that are turned on, because a disabled add-on never creates the records that trigger them. Every event has a representative sample payload behind the Send test button, so a catch-hook editor (Zapier's "Test trigger", Make, n8n) sees every mappable field before the first real delivery.
Delivery format
Each delivery POSTs a JSON envelope:
{
"event": "form.submitted",
"delivery_id": 123,
"timestamp": "2026-07-05T12:00:00+00:00",
"data": { "...": "same shape as the matching REST endpoint" }
}
The data payloads reuse the API feature's REST resource shapes, so an integration that reads the REST endpoints and one that listens to webhooks see identical fields. Deliveries also carry X-Webprocms-Event and X-Webprocms-Delivery headers, and queue after the database transaction commits — a rolled-back save never emits an event.
Firing an event from code. Webhooks::dispatch($event, fn () => $payload) writes one pending delivery row per subscribed endpoint (the payload closure runs only when something listens; webhooks:deliver-due sends the rows) and never throws — a webhook problem is reported and must never break the save that triggered it. Webhooks::dispatchOrFail() is the same without that safety net: a payload builder or a delivery insert that throws, throws. It is for a caller that records "this was announced" in the same transaction as the delivery rows, so a failed announcement rolls that record back instead of committing it with nothing behind it — automotive.lead.created fires once per lead this way, and its forward task retries an announcement that could not be written.
Using it with Zapier
- In Zapier, create a Zap with the trigger Webhooks by Zapier → Catch Hook.
- Copy the webhook URL Zapier shows and add it under Settings → Webhooks, picking the events you want.
- Click the Send test button so Zapier receives a sample request, then map its fields in your Zap.
Make, n8n, and anything else that accepts JSON POSTs work the same way.
Subscribing programmatically (REST hooks)
With the API feature also enabled, integrations can manage the same webhook rows over REST — the pattern Zapier-style platforms use to register triggers automatically:
| Endpoint | Ability |
|---|---|
GET /api/v1/webhooks |
webhooks:manage |
POST /api/v1/webhooks |
webhooks:manage |
DELETE /api/v1/webhooks/{id} |
webhooks:manage |
The create response is the only place the signing secret appears. These endpoints require both features: they 404 when either the API feature or the Webhooks feature is off.
Security notes
- Endpoints must be HTTPS; the signing secret lets receivers reject forged deliveries.
- Webhook management lives behind the admin role; managers and editors can't see or change endpoints.
- Disabling the feature 404s the settings page and stops all dispatch immediately; endpoint rows and delivery history are preserved for re-enable.