Webhooks
Webhooks let your systems react to things happening in your Usetix account — a ticket sold, a refund issued, an event going live — without polling our API. When a subscribed event occurs, Usetix sends a signed POST request to a URL you control.
You manage webhooks from your account dashboard under Settings → Webhooks.
Delivery
| Property | Value |
|---|---|
| Method | POST |
| Content type | application/json |
| User-Agent | usetix/1.0.0 Webhook |
| Timeout | 7 seconds |
| Max response size | 100 KB |
Each event produces one delivery attempt per matching webhook. There are no automatic retries today, so make sure your endpoint is available and responds quickly.
Signing
Every request is signed with HMAC-SHA256 using the webhook’s signing_secret, which is shown in the dashboard when you create the webhook. Two headers are sent:
| Header | Description |
|---|---|
X-Webhook-Signature |
Hex-encoded HMAC-SHA256 of the raw request body, keyed by the webhook’s signing secret. |
X-Webhook-Timestamp |
ISO 8601 UTC timestamp of the event (stable across any future retries). |
Verify in Ruby:
expected = OpenSSL::HMAC.hexdigest("SHA256", signing_secret, request.raw_post)
Rack::Utils.secure_compare(expected, request.headers["X-Webhook-Signature"])
Verify in Node.js:
const expected = crypto
.createHmac("sha256", signingSecret)
.update(rawBody)
.digest("hex");
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.header("X-Webhook-Signature")));
Always compute the signature over the raw request body — not a parsed or re-serialized copy — and use a constant-time comparison.
Auto-deactivation
If a webhook fails for 10 consecutive deliveries over more than 1 hour, Usetix automatically deactivates it. Deliveries stop until you reactivate the webhook from the dashboard. An endpoint returning HTTP 2xx counts as success; anything else (timeout, non-2xx, TLS error, DNS failure) counts as failure.
SSRF protection
Webhook URLs that resolve to private, loopback, link-local, or otherwise non-public IPs are rejected at delivery time. Use public hostnames only.
Subscribable events
Each webhook subscribes to one or more of the following actions:
order.paidorder.refundedorder.cancelledevent.publishedevent.unpublished
Payload envelope
Every payload has the same top-level shape. The eventable object varies by action.
{
"action": "order.paid",
"created_at": "2026-04-22T12:34:56Z",
"eventable": { "...": "see below" },
"account": {
"name": "Example Promoter",
"subdomain": "example"
}
}
Order payloads
Sent for order.paid, order.refunded, and order.cancelled. The action field tells you which transition fired.
{
"action": "order.paid",
"created_at": "2026-04-22T12:34:56Z",
"account": { "name": "Example Promoter", "subdomain": "example" },
"eventable": {
"id": "ord_c3a9f4e1",
"type": "Order",
"customer_email": "[email protected]",
"customer_name": "Jane Doe",
"total_amount": "42.00",
"currency": "EUR",
"status": "paid",
"paid_at": "2026-04-22T12:34:50Z",
"custom_field_answers": [
{
"id": 42,
"label": "Dietary preferences",
"type": "text",
"value": "Vegan"
}
],
"items": [
{
"id": "oi_4d2a8b9c",
"ticket_title": "General Admission",
"event_title": "Spring Showcase",
"price": "21.00",
"custom_field_answers": [
{
"id": 43,
"label": "Attendee name",
"type": "text",
"value": "Alice"
},
{
"id": 44,
"label": "T-shirt size",
"type": "select",
"value": "M"
}
]
}
]
}
}
| Field | Type | Notes |
|---|---|---|
eventable.id |
string | Public order ID. Stable; safe to store as your correlation key. |
eventable.customer_email |
string | Buyer’s email. |
eventable.customer_name |
string | Buyer’s name. |
eventable.total_amount |
string | Decimal encoded as a string (e.g. "42.00") to avoid float precision issues. |
eventable.currency |
string | ISO 4217 currency code. |
eventable.status |
string | paid, refunded, or cancelled. |
eventable.paid_at |
string | null | ISO 8601 UTC. null for non-paid statuses. |
eventable.custom_field_answers |
array | Order-scope custom field answers. Empty array if the event has none or the buyer left them blank. See Custom field answers. |
eventable.items[].id |
string | Public ID of the order item (one per ticket). |
eventable.items[].ticket_title |
string | Title of the ticket type. |
eventable.items[].event_title |
string | Title of the event the ticket belongs to. |
eventable.items[].price |
string | Decimal as string. |
eventable.items[].custom_field_answers |
array | Attendee-scope custom field answers for this ticket. |
Custom field answers
Organizers can define custom checkout questions per event (e.g. dietary preferences, attendee names, t-shirt sizes). Each answer is emitted as:
| Field | Type | Notes |
|---|---|---|
id |
integer | Stable numeric ID of the custom field. Use this as your key — it survives label renames. |
label |
string | Current human-readable question label, as the buyer saw it. |
type |
string | text, textarea, select, or checkbox. |
value |
string | boolean | text, textarea, and select answers arrive as strings. checkbox answers arrive as true or false. |
Answers are scoped two ways:
- Order-scope answers appear once on the order (
eventable.custom_field_answers). Used for per-checkout questions like “Dietary preferences” that apply to the whole party. - Attendee-scope answers appear per item (
eventable.items[].custom_field_answers). Used for per-ticket questions like attendee name or t-shirt size — each ticket in the order carries its own answer.
Blank answers are omitted, so an empty array means the buyer provided no answers (or the event defines no questions for that scope).
If an organizer deletes a custom field after orders were placed, the stored answers remain in Usetix but are no longer emitted on subsequent webhook deliveries for existing orders. Key your integration off id, and treat the absence of a once-seen id as “the question was removed” rather than “the answer was cleared”.
Event payloads
Sent for event.published and event.unpublished.
{
"action": "event.published",
"created_at": "2026-04-22T12:34:56Z",
"account": { "name": "Example Promoter", "subdomain": "example" },
"eventable": {
"slug": "spring-showcase",
"type": "Event",
"title": "Spring Showcase",
"starts_at": "2026-05-01T19:00:00Z",
"ends_at": "2026-05-01T23:00:00Z",
"venue": {
"name": "The Venue",
"city": "Berlin"
}
}
}
| Field | Type | Notes |
|---|---|---|
eventable.slug |
string | URL slug. The event’s public URL is https://<subdomain>.usetix.io/events/<slug>. |
eventable.title |
string | Event title. |
eventable.starts_at |
string | ISO 8601 UTC. |
eventable.ends_at |
string | ISO 8601 UTC. |
eventable.venue.name |
string | Venue name. |
eventable.venue.city |
string | Venue city. |
Testing locally
For local development, tools like ngrok or Cloudflare Tunnel give you a public URL that forwards to localhost. Point a webhook at that URL, then trigger actions in your account to see real payloads arrive.