Ir al contenido principal

Webhooks

Receive real-time HTTP POST notifications when events happen in your schedules.

Overview Pro - Requires the Pro plan

Webhooks let you receive automatic POST notifications on your own server when something happens in your schedules: a ticket is sold, an event changes, a ticket is scanned at the door. Instead of polling the API, your application is notified as it happens.

Each delivery carries an HMAC-SHA256 signature so you can verify the payload really came from Event Schedule, and every attempt is written to a delivery log you can open from your settings.

Webhooks are a Pro feature

Pro - Requires the Pro plan A webhook only fires for events that belong to a schedule on the Pro plan or above. You can add and test a webhook on any account, but if none of your schedules is Pro, nothing will ever be delivered. A selfhosted install counts as Enterprise, so webhooks are available there with no plan restriction.

Webhooks belong to your account, not to an individual schedule. One endpoint receives activity from every schedule you own, and the schedule is identifiable from the payload. Add more than one endpoint if you want to route different event types to different services.

How a delivery works

Behaviour What to expect
Method and body A single POST with a JSON body and the headers listed below.
Timeout 5 seconds. A slower endpoint is recorded as a failed delivery.
Retries Up to 3 attempts, waiting roughly 30 seconds and then 60 seconds between them.
What is retried Timeouts, connection errors and 5xx responses. Any 2xx counts as delivered, and a 4xx is treated as a permanent rejection and is not retried.
Redirects Not followed. Return your 2xx at the exact URL you registered.
Allowed endpoints Public http or https URLs only. Loopback, private, reserved and cloud metadata addresses are rejected, both when you save the webhook and again at send time.
Delivery log Every attempt is logged with its status, duration and the first part of your response. The list shows the 20 most recent, and entries are pruned after 30 days.

Setup

  1. Open Settings in the admin panel and choose Webhooks, then scroll to the Add Webhook form.
  2. Enter the Webhook URL. It has to be a publicly reachable address, so localhost and private network addresses are refused with "This URL is not allowed". Use HTTPS: the payload contains buyer names, email addresses and ticket links.
  3. Optionally add a Description, a label for your own reference that appears above the URL in the list.
  4. Under Event types, switch off anything you do not want. Every type is on by default, and leaving them all on subscribes the endpoint to everything, including any type added later.
  5. Click Add Webhook. The signing secret, a 64-character hex string, is shown once with a copy button. Store it before you leave the page: it cannot be displayed again.
  6. Send a test ping with the Test button on the saved webhook and confirm your endpoint answers with a 2xx status. The result is reported as "Test webhook sent successfully (HTTP 200)" or as a failure with the status it did get.

Managing a webhook

Each saved webhook shows its description, URL, the event types it subscribes to (or an All events badge) and when it was last triggered. The icon buttons on the right of the row do the following.

  • Enable / Disable - the tick icon pauses or resumes the webhook. A disabled webhook is dimmed in the list and receives nothing, but keeps its secret and its delivery history.
  • Test - the lightning icon sends the test payload described under Testing.
  • Edit - the pencil icon opens an inline form for the URL, description and event types. Regenerate secret sits at the bottom of that form; it issues a new secret, shows it once, and immediately invalidates the old one, so update your endpoint in the same sitting.
  • Delete - the trash icon removes the webhook and its delivery log after a confirmation.
  • View recent deliveries - the link under the row expands the last 20 attempts with the event type, response status, duration and time.

Event Types

These are the twelve types you can subscribe to. They are the same list, in the same order, as the switches on the Add Webhook form.

Event Fires when
sale.createdAn order is created: a checkout, an RSVP, an appointment booking, or a sale created through the API. At this point the sale is normally still unpaid.
sale.paidA sale is confirmed as paid, by Stripe, by Invoice Ninja, by being marked paid on the Sales page, or immediately after sale.created for a free order or RSVP.
sale.refundedA sale is refunded from the Sales page or the API.
sale.cancelledA sale is cancelled, either by the owner or by the ticket holder from their ticket page.
installment.paidA payment of an installment plan is collected. Fires once per payment rather than once per installment, so a four-part plan paid monthly sends four of these, but a buyer who settles the balance in one go sends one that clears several rows. The payload is the sale, with an installment object alongside it.
installment.failedA scheduled payment could not be collected. Sent on each failed attempt, not only the final one. Read installment.outcome to tell the two causes apart: declined is a card the bank refused, while dead_plan, duplicate, amount_mismatch and nothing_due mean money arrived but could not be applied and is waiting on the organizer. A declined attempt also carries error, attempt, is_final and next_attempt_at. A payment parked for bank authentication is not a failure and sends nothing.
event.createdAn event is published. Publishing an existing draft counts as a creation.
event.updatedA published event is saved with changes, including an appointment being rescheduled.
event.deletedA published event is deleted. The payload is captured before the row is removed.
event.cancelledAn event is cancelled rather than deleted.
ticket.scannedA ticket or pass QR code is scanned and accepted at check-in.
ticket.bookedA pass holder reserves a place on a specific date in advance.
ticket.booking_cancelledA pass holder releases a place they had reserved.
feedback.submittedAn attendee submits a rating, and optionally a comment, for an event they attended.
Drafts never fire an event webhook

Saving or deleting a draft event sends nothing. The first delivery for a draft is the event.created you get when it is published.

Group orders send one delivery per ticket holder

When one buyer checks out for several named guests, each row in the order gets its own sale.* delivery. The primary row carries the totals for the whole group; the guest rows report payment_amount as 0 so you do not count the money twice. Use is_primary and group_id in the payload to tell them apart.

A purchase that covered several events carries order_id as well, shared by every row in the order, with is_order_primary set on the one row that anchors it. Money is still reported per group, not per order, so summing payment_amount across an order_id gives the order total without double-counting.

Payload Format

Every payload uses the same three-key envelope: the type in event, an ISO 8601 timestamp, and the record itself in data. Abbreviated example of a sale.paid delivery:

{
"event": "sale.paid",
"timestamp": "2026-03-01T12:00:00+00:00",
"data": {
"id": "abc123",
"event_id": "def456",
"event_name": "Summer Concert",
"name": "Jane Doe",
"email": "jane@example.com",
"status": "paid",
"payment_amount": 25.00,
"tickets": [
{ "ticket_id": "ghi789", "quantity": 2, "price": 12.50, "type": "General" }
]
}
}

For sale.* and event.* the data object is the same record the Sales API and Events API return, so one parser can handle both. The real object carries more than the sample above: a sale also includes subdomain, phone, event_date, payment_method, transaction_reference, discount and gift-card totals, total_quantity, group_id, is_primary and timestamps, and each ticket row carries is_addon, is_pass and, for a pass, its usage counters.

Sale payloads contain the ticket secret

Unlike an API response, a sale.* webhook always includes the sale's secret, the token that opens the ticket page and its QR code. Treat the whole payload as sensitive: use HTTPS, and do not log it or forward it somewhere public.

Types with extra fields

  • ticket.scanned from a pass adds scanned_event_id and scanned_event_date, so you can tell which occurrence the pass was used on.
  • ticket.booked adds booked_event_id and booked_event_date.
  • ticket.booking_cancelled adds the same two fields plus forfeited, which is true when the release happened after the cancellation cutoff and the visit was used up.
  • feedback.submitted is the one type that does not follow the API shape. Its data holds event_id, event_name, event_date, attendee_name, attendee_email, rating and comment, and it has no id.

Request Headers

Header Description
X-Webhook-SignatureHMAC-SHA256 signature: sha256=<hex>
X-Webhook-EventThe event type (e.g. sale.paid), matching event in the body
X-Webhook-TimestampISO 8601 timestamp of when this attempt was sent. On a retry it is newer than the timestamp in the body, which is fixed when the payload is built.
Content-Typeapplication/json
User-AgentEventSchedule-Webhook/1.0

Signature Verification

Every webhook includes an X-Webhook-Signature header containing an HMAC-SHA256 hash of the raw request body, signed with your webhook secret. Always verify this signature before processing the payload.

The signature covers the request body exactly as sent and nothing else, so hash the raw bytes before any JSON parsing or re-encoding. Compare with a constant-time function, never with ==.

PHP

$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $payload, $webhookSecret);

if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}

$data = json_decode($payload, true);

Node.js

const crypto = require('crypto');

function verifyWebhook(body, signature, secret) {
const expected = 'sha256=' +
crypto.createHmac('sha256', secret).update(body).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(signature)
);
}

Python

import hmac, hashlib

def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)

Best Practices

  • Respond quickly. Return a 2xx status within 5 seconds. Queue the real work and acknowledge receipt first, or a slow database write will be recorded as a failed delivery and retried.
  • Verify signatures. Always validate the X-Webhook-Signature header before processing any payload, and reject anything that does not match.
  • Expect duplicates. A timeout on your side still counts as a failure, so a delivery you did process can arrive again. Use data.id together with event as an idempotency key, and fall back to the event and attendee for feedback.submitted, which has no id.
  • Answer at the registered URL. Redirects are not followed, so a 301 from http to https or from a bare domain to www is recorded as a failure. Register the final URL.
  • Use HTTPS. Payloads carry buyer names, email addresses and ticket secrets, so they should never cross the network in the clear.
  • Return a 4xx only when you mean it. A 4xx is treated as a permanent rejection and stops the retries; use a 5xx when you want the delivery attempted again.
  • Monitor deliveries. Open View recent deliveries in your webhook settings to debug failures. The response body you return is stored with the log, so a descriptive error message there pays for itself.

Testing

Use the Test button in your webhook settings to send a test payload. The test event uses the type webhook.test with an empty data object:

{
"event": "webhook.test",
"timestamp": "2026-03-01T12:00:00+00:00",
"data": {}
}

The test is signed and sent exactly like a real delivery, with the same headers and the same 5 second timeout, so it verifies your signature check as well as your URL. It is not retried, it ignores the event types you subscribed to, and it works whatever plan your schedules are on, which makes it the quickest way to prove the endpoint itself before you wait for real activity. The result is written to the delivery log alongside everything else.

See Also

  • REST API Reference - The same records over HTTP, for anything you need to pull rather than be pushed
  • Account Settings - Where webhooks, API keys and connected services are configured
  • Selling Tickets - The ticketing and check-in features behind the sale and scan events