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.
They can also be managed through the Webhooks API.
Personal webhooks for co-organizers
External co-organizers manage their own webhooks under Webhooks, next to API Tokens. Each personal webhook automatically follows all events currently assigned to that team membership. It supports the six order, event and ticket actions listed below; account-wide voucher actions are unavailable.
Personal payloads add "scope": "assigned_events" to the envelope. Event publication payloads retain the event format shown below. Ticket redemption payloads use order_code instead of the parent order’s public order_id.
Order payloads contain order_code, display_number, type, origin, customer_email, customer_name, customer_company, customer_phone, currency, status, paid_at, custom_field_answers, and items. Items retain the ticket fields shown below, but include only assigned events. Order answers are filtered to those same events. Whole-order IDs and shop URLs, total amounts, payment details, invoices, billing addresses, attribution and voucher data are omitted, including when every ticket currently belongs to an assigned event.
Revoking an event assignment affects existing webhooks and queued deliveries. A retry whose saved payload includes a revoked event is discarded entirely. A still-unprepared mixed order can be delivered with only its remaining assigned events. Deactivating the membership or changing its role away from co-organizer stops personal delivery. Account webhooks keep their existing payload format.
Delivery
| Property | Value |
|---|---|
| Method | POST |
| Content type | application/json |
| User-Agent | usetix/1.0.0 Webhook |
| Timeout | 7 seconds |
| Max response size | 100 KB |
Receiver failures (such as a timeout or a non-2xx response) are recorded without automatic redelivery. Internal job failures and interrupted deliveries can be retried; prepared requests keep the same body and signature. Make sure your endpoint responds quickly and handles duplicate requests safely.
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.unpublishedticket.redeemedvoucher.issuedvoucher.redeemedvoucher.refundedvoucher.adjustedvoucher.expiredvoucher.blockedvoucher.unblockedvoucher.voidedvoucher_purchase.paidvoucher_purchase.refunded
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.
eventable.shop_url is the customer link to the entire order, using the account
shop domain (including a verified custom domain). It opens without login. Use
it for buyer notifications and share it only with the buyer; it also gives
access to the order tickets and available invoice. Refunded or cancelled orders
show their current state.
{
"action": "order.paid",
"created_at": "2026-04-22T12:34:56Z",
"account": { "name": "Example Promoter", "subdomain": "example" },
"eventable": {
"id": "ord_c3a9f4e1",
"shop_url": "https://example-shop.usetix.io/orders/ord_c3a9f4e1",
"order_code": "7K3Q9D2A",
"display_number": "7K3Q-9D2A",
"type": "Order",
"customer_email": "buyer@example.com",
"customer_name": "Jane Doe",
"customer_company": "Acme GmbH",
"customer_phone": "+49 30 1234567",
"total_amount": "42.00",
"voucher_amount": "12.00",
"amount_due": "30.00",
"net_amount": "35.29",
"vat_amount": "6.71",
"vat_rate": "19.0",
"currency": "EUR",
"status": "paid",
"paid_at": "2026-04-22T12:34:50Z",
"invoice_number": "INV-1-2026-00042",
"payment_provider": "stripe",
"payment_id": "pi_3OqXyz2eZvKYlo2C0AbCdEfG",
"attribution": {
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "spring-launch",
"utm_term": "concert tickets berlin",
"utm_content": "ad-variant-a",
"ref": "partner:radiox"
},
"b2b_invoice_requested": true,
"billing_address": {
"street": "Musterstr. 1",
"postal_code": "10115",
"city": "Berlin",
"country": "DE",
"vat_id": "DE123456789"
},
"custom_field_answers": [
{
"id": 42,
"label": "Dietary preferences",
"type": "text",
"value": "Vegan"
}
],
"items": [
{
"id": "oi_4d2a8b9c",
"check_in_code": "9M5V2H8C",
"display_check_in_code": "9M5V-2H8C",
"ticket_title": "General Admission",
"event_title": "Spring Showcase",
"event_slug": "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.order_code |
string | Human-readable order code. Safe to show in customer-facing or staff-facing UI. |
eventable.display_number |
string | Formatted order code for display, typically grouped as XXXX-XXXX. |
eventable.customer_email |
string | Buyer’s email. |
eventable.customer_name |
string | Buyer’s name. |
eventable.customer_company |
string | null | Company name the buyer entered at checkout. null when not provided. |
eventable.customer_phone |
string | null | Phone number the buyer entered at checkout. null when not provided. |
eventable.total_amount |
string | Gross total, decimal encoded as a string (e.g. "42.00") to avoid float precision issues. |
eventable.voucher_amount |
string | Amount funded by one or more vouchers. |
eventable.amount_due |
string | Remainder collected by the provider; zero for a voucher-only order. |
eventable.net_amount |
string | Net component of total_amount, computed using the same allocation as the customer invoice so the two match to the cent. |
eventable.vat_amount |
string | VAT portion of total_amount (total_amount − net_amount). |
eventable.vat_rate |
string | Effective VAT rate applied, as a percentage (e.g. "19.0", "7.7", or "0.0" for VAT-exempt). |
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.invoice_number |
string | null | Customer invoice number once the invoice has been generated. null for order.cancelled and briefly between payment and invoice generation. |
eventable.payment_provider |
string | "stripe", "paypal", or "voucher" for an order fully covered by vouchers. |
eventable.payment_id |
string | null | Provider-specific payment reference. For Stripe, the payment intent ID (pi_...), paste it into the Stripe dashboard to find the charge. For PayPal, the capture ID (or order ID before capture). null until the order is paid. |
eventable.attribution |
object | Marketing attribution captured at checkout time. Always present; empty object {} if the buyer never arrived with tracking parameters. See Attribution. |
eventable.b2b_invoice_requested |
boolean | true when the buyer asked for a business invoice at checkout. |
eventable.billing_address |
object | Always present. Individual fields are null when the buyer did not request a business invoice. See Billing address. |
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[].check_in_code |
string | Human-readable ticket check-in code. Safe to show to staff and customers. |
eventable.items[].display_check_in_code |
string | Formatted check-in code for UI, typically grouped as XXXX-XXXX. |
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[].event_slug |
string | null | URL slug of the event (e.g. "spring-showcase"). Combine with your account’s shop URL to build a deep link back to the public event page. null if the event has been deleted since the order was placed. |
eventable.items[].price |
string | Decimal as string. |
eventable.items[].custom_field_answers |
array | Attendee-scope custom field answers for this ticket. |
Billing address
Populated when the buyer requested a B2B invoice at checkout (b2b_invoice_requested: true). For B2C orders the object is still present but every field is null.
| Field | Type | Notes |
|---|---|---|
street |
string | null | Street and house number. |
postal_code |
string | null | Postal / ZIP code. |
city |
string | null | City. |
country |
string | null | ISO 3166-1 alpha-2 country code (e.g. "DE", "CH", "AT"). |
vat_id |
string | null | EU VAT ID when supplied. Validated at checkout against the VIES service for cross-border EU orders. |
Attribution
The attribution object captures where the buyer came from. UTM parameters and ref are read from the checkout page’s URL, and as a fallback the previous page’s URL via the Referer header, at the moment the buyer submits the order. There is no client-side persistence and no consent banner: the data only travels with the form submission itself. Practically this means last-click attribution: the source that delivered the buyer to the checkout page is what’s recorded.
| Field | Type | Notes |
|---|---|---|
utm_source |
string | omitted | e.g. "google", "facebook", "newsletter". |
utm_medium |
string | omitted | e.g. "cpc", "email", "social". |
utm_campaign |
string | omitted | Campaign name as set in the URL. |
utm_term |
string | omitted | Paid keyword, when supplied. |
utm_content |
string | omitted | Ad / creative variant, when supplied. |
ref |
string | omitted | Free-form referral code. Usetix’s own shop footer links append ref=shop:<subdomain> when a buyer clicks through to the marketing site, so signups originating from a specific shop carry that source. |
Only fields that were actually captured are included. Buyers who arrive with no tracking parameters get "attribution": {}.
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”.
Ticket payloads
Sent once when a ticket is successfully redeemed. Duplicate or blocked redemption attempts do not produce an event. Offline redemptions produce the event when they successfully sync to Usetix.
Each redeemed ticket produces its own event; redemption events are not batched.
{
"action": "ticket.redeemed",
"created_at": "2026-05-01T18:42:17Z",
"account": { "name": "Example Promoter", "subdomain": "example" },
"eventable": {
"id": "oi_4d2a8b9c",
"order_id": "ord_c3a9f4e1",
"type": "Ticket",
"check_in_code": "9M5V2H8C",
"display_check_in_code": "9M5V-2H8C",
"event_slug": "spring-showcase",
"event_title": "Spring Showcase",
"ticket_title": "General Admission",
"place_label": "Balcony · Row A · 12",
"place_snapshot": {
"kind": "seat",
"logical_key": "balcony-a-12"
},
"redeemed_at": "2026-05-01T18:42:17Z"
}
}
| Field | Type | Notes |
|---|---|---|
eventable.id |
string | Public ticket ID. Stable; use it to deduplicate redemption events. |
eventable.order_id |
string | Public ID of the order containing the ticket. |
eventable.check_in_code |
string | Ticket check-in code. |
eventable.display_check_in_code |
string | Formatted check-in code for display. |
eventable.event_slug |
string | Slug of the event where the ticket was redeemed. |
eventable.event_title |
string | Event title stored on the purchased ticket. |
eventable.ticket_title |
string | Ticket type title stored on the purchased ticket. |
eventable.place_label |
string | null | Human-readable seat or place label, if assigned. |
eventable.place_snapshot |
object | omitted | Purchased place details, if present. |
eventable.redeemed_at |
string | ISO 8601 UTC timestamp of the successful redemption. |
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. |
Voucher ledger payloads
Sent for voucher.issued, voucher.redeemed, voucher.refunded, voucher.adjusted, voucher.expired, and voucher.voided. Every event points at the immutable entry that changed the balance. voucher.refunded means ticket-order value was restored to a voucher; voucher.voided records the remaining balance reversal after a voucher sale is refunded. voucher_purchase.refunded is the separate commercial purchase event.
{
"action": "voucher.redeemed",
"created_at": "2026-08-30T12:05:00Z",
"account": { "name": "Example Promoter", "subdomain": "example" },
"eventable": {
"id": "opaque-ledger-entry-id",
"type": "VoucherEntry",
"kind": "redemption",
"amount": "-18.50",
"balance_after": "31.50",
"currency": "EUR",
"order_id": "ord_c3a9f4e1",
"reason": null,
"metadata": {},
"voucher": {
"id": "q7R9mT2vX4pL8nK6",
"type": "Voucher",
"code": "ABCD-2345-EFGH-6789",
"product_id": "mN9uR4pKc8xQ",
"issued_amount": "50.00",
"balance": "31.50",
"available_balance": "31.50",
"currency": "EUR",
"source": "shop",
"expires_at": "2029-08-30T12:00:00Z",
"blocked_at": null,
"block_reason": null
}
}
}
amount is signed: issuance, restoration, and credit adjustments are positive; redemption, expiration, debit adjustments, and refunded-purchase reversals are negative. eventable.id is opaque and stable; use it to deduplicate ledger events. Codes are monetary bearer secrets: store them securely and do not log voucher payloads.
Voucher state payloads
voucher.blocked and voucher.unblocked use the nested Voucher shape shown above directly as eventable. A block includes blocked_at and block_reason; both are null after an unblock.
Voucher purchase payloads
Sent when a public shop voucher purchase becomes paid or fully refunded.
{
"action": "voucher_purchase.paid",
"created_at": "2026-08-30T12:00:00Z",
"account": { "name": "Example Promoter", "subdomain": "example" },
"eventable": {
"id": "K8vQ2mR7xN4pT9cL",
"type": "VoucherPurchase",
"product_id": "mN9uR4pKc8xQ",
"product_name": "Gift card",
"amount": "50.00",
"paid_amount": "40.00",
"bonus_amount": "10.00",
"currency": "EUR",
"status": "paid",
"payment_provider": "stripe",
"customer_email": "buyer@example.com",
"customer_name": "Buyer",
"recipient_email": "recipient@example.com",
"recipient_name": "Recipient",
"delivery_mode": "email_scheduled",
"scheduled_for": "2026-09-15T08:00:00Z",
"delivered_at": null,
"paid_at": "2026-08-30T12:00:00Z",
"voucher_id": "q7R9mT2vX4pL8nK6"
}
}
The purchase event contains personal data and the voucher’s opaque public ID,
but not its redeemable code. It can arrive before voucher issuance; subscribe
to voucher.issued when an integration needs the code. Treat recipient and
buyer fields under the same retention rules as order customer data, and verify
the webhook signature before processing any payload.
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.