Skip to content

Ticket Fairy developer documentation

Receive every order as it changes

Ticket Fairy sends the whole order to your URL each time it changes: the buyer, the event, every ticket, table and add-on, the money and the checkout answers. Use it to keep your CRM, access control or reporting up to date without polling.

This page describes the full order format. For the event API and the agent protocols, read the Ticket Fairy developer documentation.

Get an example payload
$ curl 'https://www.ticketfairy.com/api/v1/webhooks/notification/example?type=order&format=full_order'

No key, no sign-in. The response is JSON.

When you get a delivery

You get a delivery with the whole order when one of these things happens:

  • An order is paid. You get one delivery, after the last ticket in the order becomes valid.
  • A buyer makes the first payment on a payment plan, and again when they pay the last instalment.
  • A buyer adds an add-on to an order they already placed.
  • A ticket holder changes their details.
  • A ticket is transferred to somebody else.
  • A ticket is listed for resale, or is sold on resale.
  • A ticket is refunded or cancelled.
  • A ticket is checked in, if the event has check-in sync switched on. See the questions at the end of this page.

Set up a webhook

A webhook can cover one brand, one tour or one event. A brand webhook covers every event of that brand.

In the dashboard

  1. Open the brand, tour or event in your Ticket Fairy dashboard.
  2. Go to the Webhooks page.
  3. Add a webhook, enter your URL and choose the Full order format.

With the subscription API

Generate a webhook API key in your account settings. Send it as a Bearer token to POST https://www.ticketfairy.com/api/v1/webhooks/subscription. Set criteria to brand, tour or event, and criteria_id to its ID. Set format to full_order. source is a short name for your application, in letters, numbers, dashes and underscores. headers are sent with every delivery.

Addresses you need

Subscription endpoint, POST to subscribe and DELETE to unsubscribe
https://www.ticketfairy.com/api/v1/webhooks/subscription
Example payload, GET
https://www.ticketfairy.com/api/v1/webhooks/notification/example?type=order&format=full_order
Missed orders, GET with a Personal Access Token
https://www.theticketfairy.com/api/events/{event_id}/relationships/orders
This documentation
https://www.ticketfairy.com/developers/webhooks
Subscribe
$ curl -X POST 'https://www.ticketfairy.com/api/v1/webhooks/subscription' \
  -H 'Authorization: Bearer YOUR_WEBHOOK_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "order",
    "method": "post",
    "url": "https://example.com/ticket-fairy/orders",
    "criteria": "event",
    "criteria_id": "12345",
    "headers": {"X-Api-Key": "your-own-secret"},
    "params": {},
    "format": "full_order",
    "source": "my-crm"
  }'

The response is 201 Created with the webhook's id. Keep it. To remove the webhook, send a DELETE request with that ID. The response is 204 No Content.

Unsubscribe
$ curl -X DELETE 'https://www.ticketfairy.com/api/v1/webhooks/subscription/WEBHOOK_ID' \
  -H 'Authorization: Bearer YOUR_WEBHOOK_API_KEY'

Delivery and security

Each delivery is an HTTPS POST to your URL with a JSON body.

Headers

  • Content-Type: application/json
  • X-Signature, if the event has a signing secret. It is the HMAC-SHA256 of the raw request body, made with the signing secret, as lower-case hex.
  • The custom headers you set on the webhook.

Your response

  • Any 2xx status tells Ticket Fairy that you received the delivery.
  • Respond quickly, then process the order. Put it on your own queue first.
  • 410 Gone removes the webhook. Ticket Fairy sends no more deliveries to it.
  • Ticket Fairy does not send a failed delivery again. To catch up after a failure, read the answer to What happens if my endpoint is down?

Check the signature

Calculate the HMAC-SHA256 of the raw body with your signing secret. Compare it with X-Signature in constant time. Use the bytes you received, not JSON you parsed and encoded again, because a new encoding changes the signature.

Node.js

Node.js with Express
const crypto = require('crypto')
const express = require('express')

const app = express()
const secret = process.env.TICKET_FAIRY_SIGNING_SECRET

// Read the raw body. The signature is made from the exact bytes you receive.
app.post('/ticket-fairy/orders', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex')
  const received = Buffer.from(req.get('X-Signature') || '', 'utf8')

  if (received.length !== expected.length || !crypto.timingSafeEqual(received, Buffer.from(expected, 'utf8'))) {
    return res.sendStatus(401)
  }

  const order = JSON.parse(req.body.toString('utf8'))
  saveForLater(order) // Put the order on your own queue.
  res.sendStatus(200)
})

Python

Python with Flask
import hashlib
import hmac
import json
import os

from flask import Flask, request

app = Flask(__name__)
secret = os.environ["TICKET_FAIRY_SIGNING_SECRET"].encode()


@app.post("/ticket-fairy/orders")
def receive_order():
    raw_body = request.get_data()  # The exact bytes you received.
    expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, request.headers.get("X-Signature", "")):
        return "", 401

    order = json.loads(raw_body)
    save_for_later(order)  # Put the order on your own queue.
    return "", 200

PHP

PHP
<?php

$secret = getenv('TICKET_FAIRY_SIGNING_SECRET');
$rawBody = file_get_contents('php://input'); // The exact bytes you received.
$expected = hash_hmac('sha256', $rawBody, $secret);

if (!hash_equals($expected, $_SERVER['HTTP_X_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

$order = json_decode($rawBody, true);
saveForLater($order); // Put the order on your own queue.
http_response_code(200);

Payload

Field names are in snake_case. Every amount is an integer in the smallest unit of the currency, for example cents, or whole yen for JPY. A field with no value is null. No field is left out.

Top-level fields

Field Type What it holds
format string Always full_order.
format_version string The version of this format. Today it is 1.0.
notification_type string Always order.
sequence integer The time Ticket Fairy built this delivery, in microseconds since 1 January 1970 UTC. A delivery with a higher number carries the same or newer order.
order_id integer The order ID in Ticket Fairy. Use it as the key for the order in your system.
order_hash string The order hash, a second reference to the same order.
customer_id integer The buyer's Ticket Fairy ID.
order_status string The status of the order, in lower case, for example paid. See Status values.
order_date string The order date, in ISO 8601 format and UTC, for example 2026-10-14T19:02:11Z.
deleted_at string or null When the order was deleted, in ISO 8601 format and UTC. Null if the order is not deleted.
currency string The ISO 4217 currency code in upper case, for example USD. Every amount in the payload is in this currency.
total integer The order total. See Totals for how it adds up.
totals object The amounts that make up the total, and what is paid, due and refunded. See Totals.
promo_code string or null The promo code the buyer used on the order.
is_comp boolean True if the order is a complimentary order.
purchaser object The buyer. See Purchaser.
event object The event. See Event.
tickets array Every ticket in the order, including cancelled and refunded tickets. See Tickets.
tables array One entry for each table booking in the order. Empty if there is none. See Tables.
add_ons array The add-ons in the order. Empty if there is none. See Add-ons.
answers array The buyer's answers to the order questions at checkout. See Answers.
ticket_count integer The number of tickets in the order.
active_ticket_count integer The number of active tickets in the order.
cancelled_ticket_count integer The number of cancelled or refunded tickets in the order.

Totals

For most orders, total is subtotal plus tax. For a table booking, total also includes the balance the buyer pays at the venue, and amount_outstanding shows that balance. A refund lowers total and subtotal by the refunded amount, and amount_paid stays the same. Every field in totals is an integer.

Field What it holds
subtotal Tickets, tables and add-ons, after the discount and before tax. It includes any booking fees the buyer paid.
discount The promo code discount. It is already taken off the subtotal, so do not subtract it again.
tax The tax charged on top of the subtotal.
amount_paid What the buyer paid so far. A refund does not lower it: amount_refunded shows the refunds. It is lower than the total on a payment plan and on a table booking with a deposit. An instalment between the first and the last does not send a delivery, so this value updates with the next change to the order. A card hold that has not been charged yet counts as not paid.
amount_outstanding What the buyer still has to pay. For a table booking, this is the balance due at the venue. It is 0 once the order, or the table, is refunded or cancelled.
amount_refunded What was refunded to date, on the order, its tickets and their add-ons.

Purchaser

Field Type What it holds
first_name, last_name string or null The buyer's name.
email string or null The buyer's email address.
phone string or null The buyer's phone number.
date_of_birth string or null The buyer's date of birth, as YYYY-MM-DD.
address object The billing address: street, city, state, postcode and country. country is the ISO 3166-1 alpha-2 code, for example US.
marketing_opt_in boolean True if the buyer agreed to receive marketing email from the brand of this event at this email. It is false when the buyer checked out with an email other than their account email.

Event

Field Type What it holds
event_id integer The event ID in Ticket Fairy.
event_name string The name of the event.
start_date_time, end_date_time string or null When the event starts and ends, in the event's local time, as YYYY-MM-DD HH:MM:SS.
timezone string or null The IANA timezone of the event, for example America/Los_Angeles.
venue object The venue: name, city, state and country.

Tickets, tables and add-ons

tickets lists every ticket in the order. tables lists the table bookings, and add_ons lists the add-ons.

Ticket categories and ticket types

A ticket type is a ticket the buyer can buy at a price, for example GA Early Bird or GA Final Release. A ticket category groups ticket types, for example General Admission or VIP. Use the category to count tickets by kind of access across all releases. The category is null if the organiser did not put the ticket type in a category.

Tickets

Field Type What it holds
ticket_id integer The ticket ID in Ticket Fairy. Use it as the key for the ticket in your system.
lookup_code string The ticket code, in upper case.
barcode string The value in the ticket's QR code. If the event uses barcodes from a partner, it is that barcode.
access_status string ok if the holder can enter, or invalid if not. See Who can enter.
status string The status of the ticket, for example valid or checked in. See Status values.
cancellation_reason string or null Why the ticket was cancelled.
ticket_category_id integer or null The ticket category. Null if the organiser did not put the ticket type in a category.
ticket_category_name string or null The name of the ticket category, for example VIP.
ticket_type_id integer The ticket type.
ticket_type_name string The name of the ticket type, for example VIP Early Bird.
price integer or null The price the buyer paid for this ticket, after the discount and without tax. It includes any product extras charged with the ticket. It is 0 for a guest ticket on a table, because the table carries the price. It is null for a refunded ticket, and amount_refunded in Totals shows the refund.
promo_code string or null The promo code on the order. Every ticket in the order has the same value.
seat object or null The seat, with one field: label, the seat as it is printed on the ticket, for example Block A, seat F12 or Table 4. Null for general admission.
table_id integer or null The table booking this ticket belongs to. It matches table_id in tables.
holder object The ticket holder: first_name, last_name, email, phone, date_of_birth, age_at_event, postcode, city and country. If the holder did not give a name, email or phone, you get the buyer's. postcode, city and country come from the buyer's billing address.
tags array of strings The tags on the ticket type.
answers array The holder's answers to the ticket questions. See Answers.
notes string or null Notes on the ticket.

Tables

A table booking has one entry here. Each guest on the table has a ticket in tickets, with the same table_id and a price of 0. The table entry carries the price.

Field Type What it holds
table_id integer The table booking. Each guest ticket on the table has the same table_id.
ticket_category_id, ticket_category_name integer, string or null The ticket category of the table.
ticket_type_id, ticket_type_name integer, string The ticket type of the table.
guests_count integer The number of guests on the table.
price integer The full price of the table.
deposit_paid integer The deposit the buyer paid at checkout.
remaining integer The balance the buyer still has to pay at the venue. It drops to 0 when the venue records the payment, and when the table is refunded or cancelled.
status string or null The status of the guest tickets on the table.
ticket_ids array of integers The guest tickets on the table.

Add-ons

Field Type What it holds
add_on_line_id integer The add-on purchase. A ticket add-on has one entry for each ticket it belongs to, and those entries have the same add_on_line_id. Use add_on_line_id and ticket_id together as the key.
add_on_id integer The add-on product.
name string The name of the add-on, for example Parking pass.
group_name string or null The add-on group, for example Parking.
ticket_id integer or null The ticket this add-on belongs to. Null for an add-on on the whole order.
quantity integer How many were bought. A ticket add-on entry is always 1.
price integer The price of one.
total integer The price multiplied by the quantity.
status string The status of the add-on, for example active. See Status values.
barcode string The code to scan when the buyer collects the add-on.
answers array The answers to the add-on questions. See Answers.

Answers

The order, each ticket and each ticket add-on have an answers list with the buyer's answers to the organiser's questions.

Field Type What it holds
name string The organiser's name for the question. It does not change when the organiser edits the question text, so use it as the key.
question string The question the buyer saw. If there is no question text, it is the same as name.
answer string or null The answer.

Example payload

Maya bought two General Admission tickets at 65.00 USD and one VIP ticket at 120.00 USD, with the promo code SUMMER10 for 10% off tickets. She also bought a parking pass for the order and a T-shirt for her own ticket, and paid 8% tax.

POST body, application/json
{
    "format": "full_order",
    "format_version": "1.0",
    "notification_type": "order",
    "sequence": 1792004531482917,
    "order_id": 4815162,
    "order_hash": "9c1e5f0a7b3d",
    "customer_id": 90210,
    "order_status": "paid",
    "order_date": "2026-10-14T19:02:11Z",
    "deleted_at": null,
    "currency": "USD",
    "total": 30240,
    "totals": {
        "subtotal": 28000,
        "discount": 2500,
        "tax": 2240,
        "amount_paid": 30240,
        "amount_outstanding": 0,
        "amount_refunded": 0
    },
    "promo_code": "SUMMER10",
    "is_comp": false,
    "purchaser": {
        "first_name": "Maya",
        "last_name": "Chen",
        "email": "[email protected]",
        "phone": "+12135550143",
        "date_of_birth": "1994-03-08",
        "address": {
            "street": "120 S Main St",
            "city": "Los Angeles",
            "state": "California",
            "postcode": "90012",
            "country": "US"
        },
        "marketing_opt_in": true
    },
    "event": {
        "event_id": 12345,
        "event_name": "Sunset Festival",
        "start_date_time": "2026-11-21 14:00:00",
        "end_date_time": "2026-11-21 23:00:00",
        "timezone": "America/Los_Angeles",
        "venue": {
            "name": "Beach Park",
            "city": "Los Angeles",
            "state": "California",
            "country": "United States"
        }
    },
    "tickets": [
        {
            "ticket_id": 7001,
            "lookup_code": "K7Q2XA9M",
            "barcode": "TFK7Q2XA9M",
            "access_status": "ok",
            "status": "valid",
            "cancellation_reason": null,
            "ticket_category_id": 301,
            "ticket_category_name": "General Admission",
            "ticket_type_id": 1101,
            "ticket_type_name": "GA Early Bird",
            "price": 5850,
            "promo_code": "SUMMER10",
            "seat": null,
            "table_id": null,
            "holder": {
                "first_name": "Maya",
                "last_name": "Chen",
                "email": "[email protected]",
                "phone": null,
                "date_of_birth": "1994-03-08",
                "age_at_event": 32,
                "postcode": "90012",
                "city": "Los Angeles",
                "country": "US"
            },
            "tags": [],
            "answers": [],
            "notes": null
        },
        {
            "ticket_id": 7002,
            "lookup_code": "P4W8ZD3N",
            "barcode": "TFP4W8ZD3N",
            "access_status": "ok",
            "status": "valid",
            "cancellation_reason": null,
            "ticket_category_id": 301,
            "ticket_category_name": "General Admission",
            "ticket_type_id": 1101,
            "ticket_type_name": "GA Early Bird",
            "price": 5850,
            "promo_code": "SUMMER10",
            "seat": null,
            "table_id": null,
            "holder": {
                "first_name": "Sam",
                "last_name": "Ortiz",
                "email": "[email protected]",
                "phone": null,
                "date_of_birth": "1995-07-19",
                "age_at_event": 31,
                "postcode": "90012",
                "city": "Los Angeles",
                "country": "US"
            },
            "tags": [],
            "answers": [],
            "notes": null
        },
        {
            "ticket_id": 7003,
            "lookup_code": "R2H6LC5T",
            "barcode": "TFR2H6LC5T",
            "access_status": "ok",
            "status": "valid",
            "cancellation_reason": null,
            "ticket_category_id": 302,
            "ticket_category_name": "VIP",
            "ticket_type_id": 1201,
            "ticket_type_name": "VIP Early Bird",
            "price": 10800,
            "promo_code": "SUMMER10",
            "seat": {
                "label": "Block A, seat F12"
            },
            "table_id": null,
            "holder": {
                "first_name": "Jordan",
                "last_name": "Lee",
                "email": "[email protected]",
                "phone": null,
                "date_of_birth": "1990-12-02",
                "age_at_event": 35,
                "postcode": "90012",
                "city": "Los Angeles",
                "country": "US"
            },
            "tags": [
                "vip"
            ],
            "answers": [],
            "notes": null
        }
    ],
    "tables": [],
    "add_ons": [
        {
            "add_on_line_id": 9001,
            "add_on_id": 501,
            "name": "Parking pass",
            "group_name": "Parking",
            "price": 2500,
            "ticket_id": null,
            "quantity": 1,
            "total": 2500,
            "status": "active",
            "barcode": "a8f31c7e02d4",
            "answers": []
        },
        {
            "add_on_line_id": 9002,
            "add_on_id": 502,
            "name": "Festival T-shirt",
            "group_name": "Merchandise",
            "price": 3000,
            "ticket_id": 7001,
            "quantity": 1,
            "total": 3000,
            "status": "active",
            "barcode": "d40b9e6a1f27",
            "answers": [
                {
                    "name": "tshirt_size",
                    "question": "T-shirt size",
                    "answer": "M"
                }
            ]
        }
    ],
    "answers": [
        {
            "name": "heard_about_us",
            "question": "How did you hear about this event?",
            "answer": "Instagram"
        }
    ],
    "ticket_count": 3,
    "active_ticket_count": 3,
    "cancelled_ticket_count": 0
}

How the totals add up

  • Tickets: 5850 + 5850 + 10800 = 22500, after 2500 off with SUMMER10.
  • Add-ons: 2500 for the parking pass and 3000 for the T-shirt.
  • subtotal: 22500 + 2500 + 3000 = 28000.
  • tax: 8% of 28000 = 2240.
  • total: 28000 + 2240 = 30240, which is 302.40 USD.

Status values

Status values are lower-case words. More values can appear, so handle a value you do not know safely.

Order status

paid, payment plan, partially refunded, freefund, refunded, cancelled, pending, pending for oxxo, pending 3rd party payment, need id verification, frozen.

Ticket status

These ticket statuses let the holder in, if the order is paid, partially refunded or freefund: valid, checked in, for sale.

These ticket statuses never let the holder in. A transferred or swapped ticket is the old ticket: the new holder gets a new ticket with its own ticket_id and barcode. payment plan, sale in progress, partially refunded, transferred, swapped, sold, refunded, cancelled, unpaid, frozen.

Add-on status

active, refunded, cancelled, transferred, for sale, sold, checked in, unpaid. Accept an add-on only if its status is active.

Who can enter

access_status is ok only if both of these are true:

  • The ticket status is one that lets the holder in.
  • The order status is paid, partially refunded or freefund.

In every other case it is invalid. A ticket on a payment plan is invalid until the buyer pays the last instalment.

Store orders in your own system

Follow these steps to keep your copy of each order correct.

  1. Check the signature first, if you set a signing secret. Reject the request if it does not match.
  2. Respond quickly with a 2xx status. Put the order on your own queue and do the work after you respond.
  3. Use order_id as the key for the order and ticket_id as the key for each ticket. Each delivery holds the whole order, so replace your copy of the order, its tickets, tables and add-ons with what you receive.
  4. Keep the sequence you stored with each order. If a delivery has a lower sequence than the one you stored, ignore it.
  5. Read every amount as an integer in the smallest unit of currency. 5850 in USD is 58.50. JPY has no smaller unit, so 5850 in JPY is 5,850 yen. KWD has three decimal places, so 5850 in KWD is 5.850.
  6. Let a ticket holder in only when access_status is ok. Do not calculate it yourself from the status.
  7. access_status says whether the ticket is valid for entry. It stays ok after the ticket is scanned, so record each scan yourself and refuse a second entry unless the event allows re-entry.
  8. If deleted_at has a value, the order was deleted. Mark it as deleted in your system. Deleting an order does not send a delivery by itself, so a later delivery for that order is the first to show it.
  9. Treat a status value you do not know as one that does not let the holder in. Ignore fields you do not know.

Questions about order webhooks

How do I tell a new order from an update?

Every delivery has the same shape. If you have no order with that order_id, it is a new order. If you have one, it is an update, and the delivery replaces your copy.

Can I get the same order more than once?

Yes. One order can change several times, and you get a delivery for each change. If you subscribe the same URL to a brand and to one of its events, you get each delivery twice. Store orders by order_id and compare sequence, so a second copy does no harm.

Can deliveries arrive out of order?

Yes. The order in which deliveries arrive is not guaranteed. Compare sequence with the value you stored for that order. Keep the delivery if its sequence is higher, and ignore it if it is lower. Every delivery gets its own sequence, even when two changes happen in the same second.

What happens if my endpoint is down?

Ticket Fairy does not send a delivery again. To get the orders you missed, request GET https://www.theticketfairy.com/api/events/{event_id}/relationships/orders with a Personal Access Token as a Bearer token. Add paid=1, refunded=1 or pending=1 to get only the orders with that status. To get Freefund orders, add freefund=1. A deleted order is not in the list. Always add page and limit, for example page=1&limit=100, and request the next page until a page has fewer orders than limit. Without them, the API returns every order of the event in one response. Your role on the event must let you see its orders. This API returns orders in the dashboard format, not in the full order format.

Which value do I scan at the door?

Scan the ticket's barcode. It is the value in the QR code on the ticket. Let the holder in only if access_status is ok and you have not already let that barcode in. For an add-on, scan the add-on's barcode and accept it only if its status is active.

Why is a ticket on a payment plan invalid?

A ticket on a payment plan does not let the holder in until the buyer pays the last instalment. When they do, the order becomes paid, its tickets become valid, and you get a new delivery with access_status set to ok.

Can I add the buyer to my mailing list?

Only if purchaser.marketing_opt_in is true. It shows whether the buyer agreed to marketing email from the brand of this event.

Do I get a delivery when a ticket is checked in?

Only if the event has check-in sync switched on. To switch it on, email [email protected] with the event name.

How do I get a signing secret?

The signing secret belongs to the event. To set one, email [email protected] with the event name. When an event has a signing secret, the delivery does not send some of the custom headers you set, including Authorization, Cookie and Host. Check X-Signature to confirm that the delivery came from Ticket Fairy, not an Authorization header.

Can I get the delivery as GET, PUT or PATCH?

No. The full order format is always sent as a POST with a JSON body, whatever method you choose.

How do I test my endpoint?

Request https://www.ticketfairy.com/api/v1/webhooks/notification/example?type=order&format=full_order. It returns an example full order payload with no credential. Send it to your endpoint to check that your code reads it.

How do I stop the deliveries?

Remove the webhook on the Webhooks page in your dashboard, or send DELETE /api/v1/webhooks/subscription/{id}. You can also respond to a delivery with 410 Gone. Ticket Fairy then removes that webhook and sends no more deliveries to it.

Questions about webhooks?

Email [email protected] with the webhook ID, the host name it sends to, the order ID and the response your endpoint sent. Leave out the rest of the address, because it can hold an access key.