Webhooks
Webhooks#
Webhooks send a POST request to an external URL when something happens in the plugin. Use them to connect other services, trigger automations, or sync data.
How webhooks work#
- You create a webhook through the REST API with a URL and one topic
- When that topic fires, the plugin queues a delivery with WP-Cron
- Your server receives a signed JSON payload
There is no admin screen for webhooks. Management is done through the REST API only. The Webhooks checkbox under Events > Settings > Advanced is saved but not read by the delivery code, so it has no effect.
Managing webhooks#
All routes except topics require the manage_options capability. See the REST API page for authentication.
| Method | Route | Purpose |
|---|---|---|
| GET | /wp-json/emwc/v1/webhooks | List webhooks |
| POST | /wp-json/emwc/v1/webhooks | Create a webhook |
| GET | /wp-json/emwc/v1/webhooks/{id} | Get one webhook |
| PUT | /wp-json/emwc/v1/webhooks/{id} | Update a webhook |
| DELETE | /wp-json/emwc/v1/webhooks/{id} | Delete a webhook |
| POST | /wp-json/emwc/v1/webhooks/{id}/test | Send a test delivery |
| GET | /wp-json/emwc/v1/webhooks/topics | List topics (public) |
Webhook fields#
| Field | Required | Description |
|---|---|---|
name | On create | Reference name |
url | On create | Delivery URL |
topic | On create | One topic per webhook (see below) |
secret | No | Signing secret. A random 32-character secret is generated if omitted |
status | No | active (default), paused, or disabled |
Responses also include id, created_at, updated_at, delivery_url, and failures. The secret is returned in GET responses, so restrict who holds manage_options.
Create a webhook#
curl -X POST \
-H "Content-Type: application/json" \
-u admin:application_password \
-d '{"name":"CRM sync","url":"https://example.com/hook","topic":"attendee.created","secret":"my-secret"}' \
https://yoursite.com/wp-json/emwc/v1/webhooks
Test a webhook#
curl -X POST -u admin:application_password \
https://yoursite.com/wp-json/emwc/v1/webhooks/1/test
The test is sent straight away (not through cron) with topic test and "test": true in the payload. The response contains success and response_code. Test deliveries count toward the failure limit described below.
Webhooks are stored in the emwc_webhooks option. The last 100 deliveries are stored in the emwc_webhook_logs option. There is no endpoint for reading the log.
Topics#
Only one topic can be set per webhook. Create several webhooks to cover several topics.
Topics that fire#
| Topic | Fires when |
|---|---|
event.created | An event is created in the admin or through the REST API |
event.updated | An event is saved in the admin or updated through the REST API |
event.deleted | An event is permanently deleted, or deleted through the REST API |
attendee.created | An attendee record is created when tickets are generated for an order |
attendee.updated | An attendee is updated through PUT /attendees/{id} |
attendee.checked_in | An attendee is checked in (admin or REST) |
attendee.checked_out | An attendee is checked out (admin or REST) |
Events created or updated through the REST API fire once, from the REST action.
Topics that are listed but never fire#
These are accepted by the topic field and appear in /webhooks/topics, but nothing in the plugin triggers them: attendee.cancelled, ticket.sold, ticket.refunded, capacity.reached, event.reminder.
Webhook payload#
Payload structure#
{
"topic": "attendee.created",
"webhook_id": 1,
"timestamp": "2025-01-15 10:30:00",
"data": {}
}
timestamp is the site’s local time in Y-m-d H:i:s format.
Event payload (event.created, event.updated)#
{
"data": {
"id": 123,
"title": "Tech Conference",
"status": "publish",
"start_date": "2025-03-15",
"start_time": "09:00",
"end_date": "2025-03-15",
"end_time": "17:00",
"venue_id": "45",
"organizer_id": "12",
"capacity": "500",
"event_status": "scheduled",
"url": "https://yoursite.com/events/tech-conference/"
}
}
Event deleted payload#
{
"data": {
"id": 123,
"force": true
}
}
Attendee payload (all attendee.* topics)#
{
"data": {
"id": 456,
"event_id": 123,
"event_name": "Tech Conference",
"order_id": 789,
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"ticket_code": "EMWC-ABC123DEF",
"status": "checked_in"
}
}
Security#
Request headers#
| Header | Value |
|---|---|
Content-Type | application/json |
X-EMWC-Webhook-ID | Webhook ID |
X-EMWC-Topic | Topic name |
X-EMWC-Signature | HMAC-SHA256 of the raw body, hex encoded |
X-EMWC-Delivery | Unique delivery UUID |
X-EMWC-Timestamp | Same value as timestamp in the payload |
Signature verification#
The signature is the plain hex digest. There is no sha256= prefix.
$payload = file_get_contents( 'php://input' );
$signature = $_SERVER['HTTP_X_EMWC_SIGNATURE'];
$expected = hash_hmac( 'sha256', $payload, $your_secret );
if ( hash_equals( $expected, $signature ) ) {
// Valid webhook
}
HTTPS#
Use HTTPS URLs. SSL certificates are verified on delivery.
Delivery#
Timing#
Deliveries are queued with wp_schedule_single_event on the emwc_deliver_webhook hook and sent on the next WP-Cron run. If WP-Cron is disabled on your site, make sure a system cron calls wp-cron.php.
Timeout#
Requests time out after 30 seconds and follow up to 5 redirects. Return a 2xx status quickly and process the payload afterwards.
Retries#
Each delivery is attempted once. The plugin contains a retry queue handler on the emwc_webhook_retry hook, but nothing schedules it and nothing adds failed deliveries to the queue, so failed deliveries are not retried.
Failure handling#
| Response | Result |
|---|---|
| Status below 400 | Success, failure counter reset to 0 |
| Status 400 or higher | Logged as failure, counter incremented |
| Connection error or timeout | Logged as failure, counter incremented |
After 5 consecutive failures the webhook status is set to disabled. Set status back to active with a PUT request to resume.
Pausing#
Set status to paused to stop deliveries. Paused webhooks skip triggers; nothing is queued for later.
Use cases#
Slack notifications#
- Create a Slack Incoming Webhook
- Create a webhook with the Slack URL and topic
attendee.created - New registrations post to Slack
Zapier integration#
- Create a Zapier webhook trigger
- Use the Zapier URL as the webhook
url - Build Zaps with the payload data
CRM sync#
- Create an endpoint in your CRM or middleware
- Create a webhook with topic
attendee.created - Create contacts from the payload
Troubleshooting#
Webhook not firing#
- Check
statusisactive(it becomesdisabledafter 5 failures) - Check the topic is one that fires (see the list above)
- Check WP-Cron is running on the site
- Check the REST API is enabled under Events > Settings > Advanced
Delivery failures#
- Check the URL is reachable and the certificate is valid
- Check the server responds within 30 seconds
- Read the
emwc_webhook_logsoption for response codes and error messages
Signature validation failing#
- Verify the secret matches the one stored on the webhook
- Hash the raw request body, not re-encoded JSON
- Compare against the plain hex digest without a prefix