Webhooks API

Get a signed POST to your own URL the moment something happens in your organisation, instead of polling. Covers subscribing, the event envelope, signature verification, retries and the event catalogue.

A webhook lets Seaty tell your system the moment something happens, instead of you polling the API. You register an https URL, and Seaty sends it a signed POST for each event you subscribe to.

Subscribing

Webhooks are managed in the admin dashboard, not via the API. Go to your organisation's Developer tools, open the Webhooks tab, and:

  1. Enter your endpoint URL (must be https://).
  2. Tick the events you want.
  3. Save. A signing secret (whsec_...) is shown, you will use it to verify deliveries. You can read it again from the list at any time.

You can pause (disable), edit, delete, and send a test ping to a webhook from the same screen. The delivery log shows recent attempts with their status and response code; open any delivery to inspect the exact payload that was sent and the response your endpoint returned, and to resend it. For successful deliveries the request and response bodies are not retained (only failed or retrying deliveries keep them, for diagnosis and resend), so a delivered row shows no body.

Your endpoint must be publicly reachable. The URL has to be https:// and resolve to a public internet address. Seaty refuses to deliver to localhost, private/internal addresses, or cloud-metadata hosts: such attempts are recorded as blocked in the delivery log and never sent. Test with a public tunnel (for example an ngrok https URL) rather than an internal hostname.

The delivery

Every delivery is an HTTP POST with a JSON body in this envelope:

{
  "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "type": "balance.payment_recorded",
  "organisation_id": 24,
  "organisation_tag": "wmtc",
  "event_id": 1024,
  "event_tag": "spring-show",
  "created": "2026-06-04T12:00:00Z",
  "data": {
    "id": 90233,
    "guid": "f1c2...",
    "event_id": 1024,
    "payee_email": "buyer@example.com",
    "amount": 1500,
    "type_id": 1,
    "is_refund": false,
    "date_payment_made": "2026-06-04T00:00:00Z",
    "notes": "Cash on the door"
  }
}

The envelope always carries the context of who and what the event is about, so you do not have to dig into data to route it:

FieldMeaning
organisation_id / organisation_tagThe organisation the event belongs to (numeric id and the URL tag).
event_id / event_tagThe Seaty event (the show/production) the event relates to, when applicable. Both are null for events not tied to a single event (and for the test ping).

The data object matches the shape of the matching read endpoint (so a balance.payment_recorded payload looks like a payment from an order's payments). Alongside the body, these headers are sent:

HeaderMeaning
X-Seaty-EventThe event type, e.g. balance.payment_recorded.
X-Seaty-DeliveryA unique id for this delivery attempt.
X-Seaty-Signaturet=<unix-timestamp>,v1=<hmac> (see below).

Verifying the signature

Always verify. Compute an HMAC-SHA256 over "<timestamp>.<raw request body>" using your signing secret, and compare it to the v1 value in the X-Seaty-Signature header. Use the raw body bytes exactly as received, before any JSON parsing.

import hashlib
import hmac

def is_valid(secret: str, signature_header: str, raw_body: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    expected = hmac.new(
        secret.encode(),
        f"{parts['t']}.{raw_body}".encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Reject the request if it does not match. Two things make this robust:

  • Use a constant-time comparison (e.g. hmac.compare_digest, as above), never ==, so you do not leak the signature through timing.
  • Reject stale timestamps to limit replay. Treat the delivery as invalid if t is more than 5 minutes from your own clock (abs(now - t) > 300). The t value is the unix timestamp in the X-Seaty-Signature header. Combined with deduplicating on the envelope id, this stops a captured payload from being replayed against you later.

Responding, retries and ordering

  • Return any 2xx quickly (within 10 seconds) to acknowledge receipt. Do the real work afterwards.
  • Any non-2xx, a timeout, or a connection error is treated as a failure and retried with increasing backoff (about 1 min, 5 min, 30 min, then 2 hours, up to 5 attempts) before the delivery is marked failed. Redirects are not followed (a 3xx counts as a failure), so point the webhook at its final URL.
  • Delivery is at least once: a retry can arrive after you already processed one. The envelope id is a unique id per event (not the resource id, which is in data) and stays the same across retries of that event, so deduplicate on id.
  • Order is not guaranteed. Use created if you need to reason about sequence.

When an endpoint keeps failing

If deliveries to an endpoint keep failing, Seaty stops wasting attempts on it:

  • After about 20 consecutive failed deliveries, or an endpoint that has been failing continuously for roughly 24 hours, the endpoint is automatically disabled and the organisation's administrators are emailed. No further events are sent to it until it is re-enabled.
  • Fix the endpoint (make sure it is publicly reachable and returns a 2xx), then re-enable it from the dashboard. Re-enabling clears the failure count and resumes delivery. Events that fired while it was disabled are not back-filled.
  • Separately, Seaty can suspend all webhooks for an organisation as a rare anti-abuse measure. While suspended, deliveries stop and creating or testing a webhook returns an error asking you to contact support@seaty.co.uk.

Event catalogue

Event typeFires whendata
balance.payment_recordedAn external balance payment is recorded against an event.The payment (see Balance payments). amount is always positive pence; is_refund gives the direction.
balance.refund_recordedAn external balance refund is recorded against an event.The refund (same shape, is_refund: true, amount still positive pence).
order.placedAn order is created: a customer purchase, an admin or comp order, or an accepted ticket request. is_admin_order is true for orders placed by an organiser in the admin tools.id, order_guid, event_date_id, email, attendee_name, ticket_count, is_admin_order, and the money breakdown below (all pence).
ticket.checked_inA ticket is checked in (scanned and admitted) for the first time. Re-scans of an already-admitted ticket do not fire.ticket_guid, order_id, event_date_id, checked_in_at.
request.madeA customer makes a ticket request that is awaiting approval.id, event_date_id, email, status, ticket_count, subtotal, discount (an object, or null when there is no discount), discount_total, total (pence). A request is pre-approval, so it has no fees and nothing is paid yet.

order.placed money breakdown

All amounts are integer pence.

FieldMeaning
subtotalTicket face value, before discount.
discounts[]Each applied discount: name, code, amount. An empty array [] when there are none.
discount_totalSum of discounts[].amount.
feeshandling, service (Seaty/card fee) and organisation fee amounts. Populated on the card-payment path; 0 for comp/admin orders.
fee_modeabsorbed (the organiser pays the service and organisation fees, so the customer is not charged them) or passed_on (the customer pays them on top).
donationOptional donation added to the order.
enhanced_refund_feeRefund-protection fee, if the customer took it.
totalWhat the customer was charged: subtotal - discount_total + donation + handling + enhanced_refund_fee, plus service + organisation when fee_mode is passed_on.

order.placed fires when the order is created, which is just before the payment row is written. For the authoritative settled figures (amount_paid, amount_refunded, balance, the full payments list), read the order through the Orders API.

A ping event is also delivered when you use Send test in the dashboard, so you can check your signature handling before going live.

More event types (order updates, request approvals, and others) will be added to this catalogue as they come online. Subscribing only ever delivers the events that exist.


Need help? Email support@seaty.co.uk.