Tito API
One JSON API per account, served by the account itself: events, orders, tickets, check-in, reports and webhooks, plus the whole account as a SQLite file.
This is the generic copy of the reference. Every account serves its own at https://<yourslug>.go.tito.io/docs/api with that account's real event in every example.
Base URL https://<yourslug>.go.tito.io/api/v1, or the root-domain form https://go.tito.io/<yourslug>/api/v1: either host, same key, same answers.
Try requests from this page
Open this page on one of your own accounts to send requests against it, or paste an existing key.
Using in every example below.
curl https://<yourslug>.go.tito.io/api/v1/events \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"events": [
{
"slug": "annual-conf",
"path": "annual-conf",
"name": "Annual Conference 2026",
"description": "Two days of talks, workshops and hallway conversations.",
"venue": "The Round Room, Dublin",
"venue_address": "Rotunda, Parnell Square, Dublin 1",
"venue_lat": "53.3529",
"venue_lng": "-6.2634",
"venue_place_id": "",
"map_provider": "google",
"starts_at": "2026-10-14T09:00:00Z",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"draft": false,
"secret": false,
"no_index": false,
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
]
}
Authentication
Every request carries an account API key as a bearer token. Keys are created at Settings → API in the account admin, shown once, and stored hashed. If you lose one, create another.
A key carries a set of capabilities, the same permissions the admin's roles use (events.view, orders.manage, checkin.manage, export.pii, …). Each endpoint names the capability it needs; a key without it gets 403. The temporary key this page mints is read-only.
Keys minted on a test-mode account (sandbox Stripe) start with titogo_sk_test_; everything else starts with titogo_sk_. The prefix is descriptive: the account is either test or live, and there is no switch inside it.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf \
-H "Authorization: Bearer titogo_sk_your_key"
{
"error": {
"code": "missing_key",
"message": "send Authorization: Bearer <api key>"
}
}
{
"error": {
"code": "missing_capability",
"message": "this key does not carry orders.view"
}
}
Errors
Every error is the same shape: an HTTP status that says what class of problem it is, and a JSON body with a machine code and a sentence for a human. Check the status first, then the code.
200It worked. The body is the resource.401missing_keyorunknown_key: no bearer header, or a key that was revoked or has expired.403missing_capability: the key is valid but doesn't carry what this endpoint needs. The message names the capability.404not_found: no such event, order, or list. Also any path not in this reference.422A write's body didn't validate. The message says which field.500internal: our fault. Safe to retry; if it persists, the request is in the account's API log at Settings → API.
Error messages are always English; they're for logs and developers, not for the people buying tickets.
{
"error": {
"code": "not_found",
"message": "no such event"
}
}
Pagination
Lists that can grow (orders, tickets, workflow runs) are cursor-paged, newest first. Each page carries next_after: pass it back as ?after= to get the next page. When it's null, you've seen everything.
?limit= sets the page size (default 50, maximum 200). A full final page still returns a cursor, which then yields an empty page. Loop until the list comes back empty or next_after is null, whichever you prefer.
Events and check-in lists aren't paged: an account has tens of those, not thousands.
curl "https://<yourslug>.go.tito.io/api/v1/events/annual-conf/orders?limit=100" \
-H "Authorization: Bearer titogo_sk_your_key"
# then, with the next_after from that page:
curl "https://<yourslug>.go.tito.io/api/v1/events/annual-conf/orders?limit=100&after=H7K2PX" \
-H "Authorization: Bearer titogo_sk_your_key"
Conventions
| Money | Integer minor units (amount_cents: 14900 is €149.00; a zero-decimal currency like JPY has none) with a lowercase ISO 4217 currency beside every amount. Never a float, never a formatted string. |
|---|---|
| Time | RFC 3339 in UTC (2026-10-14T09:00:00Z). A time that hasn't happened yet is null: an unpaid order's paid_at, for instance. |
| Identifiers | Events by slug. Orders and tickets by ref, the short code printed on a ticket. Refs are unique across the account and safe to quote in support, but they are names, not secrets. A buyer's order-page link is never returned by the API. |
| Writes | Bodies are JSON with Content-Type: application/json. Machine checkout accepts an Idempotency-Key header so a retried create never makes two orders. |
| Lists | An empty list is [], never null. |
| Rate limits | None yet. Be reasonable; an integration that hurts an account will be asked to slow down before anything automated is. |
| Logging | Every request, including denied ones, is recorded against the key that made it and shown at Settings → API for 90 days. |
{
"amount_cents": 29800,
"currency": "eur",
"created_at": "2026-05-18T14:02:11Z",
"paid_at": "2026-05-18T14:03:07Z",
"refunded_at": null
}
Account
The account itself: its slug, its display name, and where its data currently lives.
Get the account
GET /account
The account's own identity: slug, display name, and — where the platform places accounts by region — the region its data currently lives in.
Requires events.view
Response fields
regionstring- Where the account's data currently lives, on deployments that place accounts by region. Absent on a standalone instance with no placement — never an empty string.
Responses
200The account calling.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/account \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/account", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"slug": "acme",
"name": "Acme Events",
"region": "uk"
}
Events
Events are the things you sell tickets to, and series group them. Every other resource hangs off an event, addressed by its slug.
List events
GET /events
Requires events.view
Response fields
eventsarray of Eventevents[].pathstring- The event's public address. Equal to the slug for an event in no series; for one inside a series the series carries the first segment, so the address reads "meet-tito/london" while the slug stays "meet-tito-london". Every endpoint here is keyed on the slug — link buyers to the path.
events[].venue_addressstring- Organizer-entered address text, in the event's default locale. Empty when no structured venue is set.
events[].venue_latstring- Decimal latitude of the venue's map pin, as a string. Empty when the event has no pin.
events[].venue_lngstring- Decimal longitude of the venue's map pin, as a string. Empty when the event has no pin.
events[].venue_place_idstring- Google's opaque place id for the venue, if the address was chosen from the picker. Empty otherwise.
events[].map_providerstring- Which service the organizer asked to draw the map on the event page. A stored preference, not a promise about what people see: an event set to "apple" on an instance without Apple credentials falls back to the Google embed, and an event with no map credentials at all shows the address and its open-in links.
- one of
googleapple events[].starts_atstring (RFC 3339) or null- Null when the event has no start time set (stored as Unix 0).
events[].currenciesarray of string- Every currency the event sells in:
currencyfirst, then the extra currencies the organizer priced. A one-currency event lists justcurrency. events[].currency_assignmentstring- How a buyer lands in one of
currencies: a picker on the event page, the browser's country, or only a link carrying?currency=. Meaningful only whencurrencieshas more than one entry. - one of
switcherlocalelink events[].created_atstring (RFC 3339) or null- Null when the underlying Unix timestamp is 0 (should not occur in practice for created_at, but the same nullable encoding is used for every timestamp field).
events[].draftboolean- True while the event is a draft: its public page answers 404 and it is absent from the account's public event list. A duplicated event lands as a draft until it is published.
events[].secretboolean- True when the event is secret: finished and selling, but deliberately not advertised. Unlike no_index this DOES gate visibility and a listing must respect it — the event's main page answers 404 to everybody but a signed-in organizer, and the event is absent from the account's public event list and from its series page. It is not a second draft: a draft can take no money, while a secret event sells all day through every door its organizer handed somebody on purpose (audience pages, portal links, invitations, and order and ticket links already sent). Read it as "do not advertise", never as "closed for business".
events[].no_indexboolean- True when the organizer has asked search engines not to list this event. Unlike draft this gates NOTHING: the event's page answers exactly as it did before, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells. It is independent of secret in both directions, so this flag says nothing about whether the event is reachable — read secret for that. All it does is add a robots noindex directive to the event's own page, at every address that page answers on. Do not treat it as a visibility flag; the one thing it should change for an integration is that a page rebuilt elsewhere carries the same request across.
events[].seriesobject- The series this event belongs to, absent when it is in none. An event belongs to at most one.
events[].series.slugstringevents[].series.namestringevents[].fieldsobject- The event's DETAILS — content the organizer typed about this event (a video-call link, a hashtag) under Settings, Metadata, keyed by the detail's short name, and what a theme reads as event.fields.<key>. Only details about the EVENT appear; anything about a person is a per-attendee answer or a per-attendee detail (some of them internal) and is never included. The value is resolved: a detail this event has not answered for itself reports its account default, because that is what every page and email will show. A detail with no value anywhere is omitted rather than reported blank — blank is a normal answer for a detail. Every value is a string, a number detail included: it reports its digits with no grouping (
50000), so it parses, while the organizer's own screens and a themed page write it out for their reader's language. Present on the single-event endpoint only, and omitted entirely when the event has nothing set. events[].sectionsarray of EventSection- The event page's sections, in the order people meet them — what a ticket type's section_id points at. Present on the single-event endpoint only.
events[].sections[].idintegerevents[].sections[].namestring- The heading people read above this section. Empty when the section deliberately has none — its things render with nothing above them. A section still carrying one of the platform's own default headings reports it in English; this API negotiates no locale.
events[].sections[].positioninteger- Zero-based place in the page, top to bottom. The list is already returned in this order.
Responses
200Events, newest first.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"events": [
{
"slug": "annual-conf",
"path": "annual-conf",
"name": "Annual Conference 2026",
"description": "Two days of talks, workshops and hallway conversations.",
"venue": "The Round Room, Dublin",
"venue_address": "Rotunda, Parnell Square, Dublin 1",
"venue_lat": "53.3529",
"venue_lng": "-6.2634",
"venue_place_id": "",
"map_provider": "google",
"starts_at": "2026-10-14T09:00:00Z",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"draft": false,
"secret": false,
"no_index": false,
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
]
}
Get an event
GET /events/{slug}
Get an event by slug.
Requires events.view
Path parameters
slugstring required- The event's slug.
Response fields
pathstring- The event's public address. Equal to the slug for an event in no series; for one inside a series the series carries the first segment, so the address reads "meet-tito/london" while the slug stays "meet-tito-london". Every endpoint here is keyed on the slug — link buyers to the path.
venue_addressstring- Organizer-entered address text, in the event's default locale. Empty when no structured venue is set.
venue_latstring- Decimal latitude of the venue's map pin, as a string. Empty when the event has no pin.
venue_lngstring- Decimal longitude of the venue's map pin, as a string. Empty when the event has no pin.
venue_place_idstring- Google's opaque place id for the venue, if the address was chosen from the picker. Empty otherwise.
map_providerstring- Which service the organizer asked to draw the map on the event page. A stored preference, not a promise about what people see: an event set to "apple" on an instance without Apple credentials falls back to the Google embed, and an event with no map credentials at all shows the address and its open-in links.
- one of
googleapple currenciesarray of string- Every currency the event sells in:
currencyfirst, then the extra currencies the organizer priced. A one-currency event lists justcurrency. currency_assignmentstring- How a buyer lands in one of
currencies: a picker on the event page, the browser's country, or only a link carrying?currency=. Meaningful only whencurrencieshas more than one entry. - one of
switcherlocalelink created_atstring (RFC 3339) or null- Null when the underlying Unix timestamp is 0 (should not occur in practice for created_at, but the same nullable encoding is used for every timestamp field).
draftboolean- True while the event is a draft: its public page answers 404 and it is absent from the account's public event list. A duplicated event lands as a draft until it is published.
secretboolean- True when the event is secret: finished and selling, but deliberately not advertised. Unlike no_index this DOES gate visibility and a listing must respect it — the event's main page answers 404 to everybody but a signed-in organizer, and the event is absent from the account's public event list and from its series page. It is not a second draft: a draft can take no money, while a secret event sells all day through every door its organizer handed somebody on purpose (audience pages, portal links, invitations, and order and ticket links already sent). Read it as "do not advertise", never as "closed for business".
no_indexboolean- True when the organizer has asked search engines not to list this event. Unlike draft this gates NOTHING: the event's page answers exactly as it did before, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells. It is independent of secret in both directions, so this flag says nothing about whether the event is reachable — read secret for that. All it does is add a robots noindex directive to the event's own page, at every address that page answers on. Do not treat it as a visibility flag; the one thing it should change for an integration is that a page rebuilt elsewhere carries the same request across.
seriesobject- The series this event belongs to, absent when it is in none. An event belongs to at most one.
series.slugstringseries.namestringfieldsobject- The event's DETAILS — content the organizer typed about this event (a video-call link, a hashtag) under Settings, Metadata, keyed by the detail's short name, and what a theme reads as event.fields.<key>. Only details about the EVENT appear; anything about a person is a per-attendee answer or a per-attendee detail (some of them internal) and is never included. The value is resolved: a detail this event has not answered for itself reports its account default, because that is what every page and email will show. A detail with no value anywhere is omitted rather than reported blank — blank is a normal answer for a detail. Every value is a string, a number detail included: it reports its digits with no grouping (
50000), so it parses, while the organizer's own screens and a themed page write it out for their reader's language. Present on the single-event endpoint only, and omitted entirely when the event has nothing set. sectionsarray of EventSection- The event page's sections, in the order people meet them — what a ticket type's section_id points at. Present on the single-event endpoint only.
sections[].idintegersections[].namestring- The heading people read above this section. Empty when the section deliberately has none — its things render with nothing above them. A section still carrying one of the platform's own default headings reports it in English; this API negotiates no locale.
sections[].positioninteger- Zero-based place in the page, top to bottom. The list is already returned in this order.
Responses
200The event.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"slug": "annual-conf",
"path": "annual-conf",
"name": "Annual Conference 2026",
"description": "Two days of talks, workshops and hallway conversations.",
"venue": "The Round Room, Dublin",
"venue_address": "Rotunda, Parnell Square, Dublin 1",
"venue_lat": "53.3529",
"venue_lng": "-6.2634",
"venue_place_id": "",
"map_provider": "google",
"starts_at": "2026-10-14T09:00:00Z",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"draft": false,
"secret": false,
"no_index": false,
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
List ticket types
GET /events/{slug}/ticket-types
List an event's ticket types. Archived ticket types are not included.
Requires events.view
Path parameters
slugstring required- The event's slug.
Response fields
ticket_typesarray of TicketTypeticket_types[].idintegerticket_types[].namestringticket_types[].price_centsinteger- Integer cents; see currency. On a ticket with price tiers this is the LIVE rung's price — what a buyer would be charged right now — so a caller that only wants the price needs to know nothing about tiers. Between two tier windows with no bridge price set it falls back to the type's own dormant base and nothing can be bought;
tiersis what tells the two apart. ticket_types[].currencystring- Lowercase ISO 4217 currency code — the event's currency; ticket types carry no currency of their own.
ticket_types[].pricesobject- The type's price in every currency it is offered in, minor units keyed by lowercase currency code —
currency→price_centsalways, plus each extra currency the organizer priced it in. A currency the event sells in but this type has no key for is one the type is not offered in. ticket_types[].on_salebooleanticket_types[].quantityinteger or null- Capacity for this type: null when the type is uncapped, otherwise how many exist. 0 is a real answer and means none available — the type reads as sold out. Until 2026-08-12 this was always an integer and 0 meant unlimited, which left no way to say sold out; a caller that special-cased 0 must now special-case null.
ticket_types[].soldinteger- Count of this type's tickets that are still status='valid' — the same rule the admin's sold count uses. A voided ticket drops out.
ticket_types[].tiersarray of TicketTier- The ticket's price ladder, in the order people move through it. Absent entirely for a ticket priced once, which is most of them. A tiered ticket is still ONE ticket type — one row on the event page, one set of questions, one cap — that charges a different amount as tickets sell or as dates pass. This is the LIVE ladder: a tier the organizer removed after it had sold tickets is hidden from it, because nobody can buy at that tier again — but its sales are still counted in the type's own
sold, so the tiers here will not always add up to it. The organizer's Sales report is where a removed tier is still named. ticket_types[].tiers[].idintegerticket_types[].tiers[].namestring- The rung's public name, and often empty — a blank name is a real answer, and people then see the price on its own.
ticket_types[].tiers[].quantityinteger or null- This rung's own allocation, on the same convention as the type's: null when uncapped, otherwise how many exist, and 0 means none available. It is NOT a share of the type's total — the type's total is the sum of the rungs. What the rung can actually sell may be larger than this, because a rung whose window closed with tickets unsold rolls them forward to the next one.
ticket_types[].tiers[].soldinteger- Seats bought at this rung, from paid orders. Every order line records the rung it was bought at, so this stays attributed to the price actually paid even after the ladder has moved on — which is also what returns a refunded ticket to the right rung.
ticket_types[].tiers[].starts_atstring (RFC 3339) or null- When the rung opens; null for no start. RFC 3339.
ticket_types[].tiers[].ends_atstring (RFC 3339) or null- When the rung closes, EXCLUSIVE — the same contract as a product's sales_end_at. null for no end. RFC 3339.
ticket_types[].tiers[].liveboolean- Whether this is the rung on sale right now. At most one rung is live, and none is when the ladder sits between windows or every rung is spent.
ticket_types[].tiers[].pricesobject- This rung's price in every currency it is offered in, minor units keyed by lowercase currency code — the type's own
currency→price_centsalways, plus each extra currency the organizer priced the rung in. A currency the event sells in but this rung has no key for is one the type is not offered in while this rung is live. ticket_types[].bandsarray of TicketBand- The ticket's volume ladder: the price drops as the order gets bigger, and the WHOLE order takes one band's price rather than a graduated mix. Absent entirely for a ticket that has none, which is most of them.
Unlike
tiers, this does NOT moveprice_cents: a tier's price is what everybody pays right now, so the type reports it, while a band's price is what a big enough order pays. Soprice_centsis always what ONE ticket costs, and a caller pricing a basket resolves the band itself — the last band whosefrom_qtyis at or below the quantity, elseprice_cents. A caller that skips this and multipliesprice_centsby six will quote a figure checkout does not charge.The quantity that selects a band is the number of THIS type in ONE order, counting only the tickets being paid for — a ticket earned free by a group discount does not count towards a band.
Never present alongside
tiers: a ticket has one ladder or the other. ticket_types[].bands[].from_qtyinteger- The order quantity this band starts at — two or more, always. One ticket is the type's own price_cents.
ticket_types[].bands[].price_centsinteger- What ONE ticket costs from from_qty upwards. Integer cents; see the type's currency.
ticket_types[].bands[].pricesobject- This band's price in every currency it is offered in, minor units keyed by lowercase currency code — the type's own
currency→price_centsalways, plus each extra currency the organizer priced the band in. A currency the type is sold in but this band has no key for is one the band is not offered in: an order in that currency pays the band above it. ticket_types[].positioninteger- This type's place on the event page, in the one position space it shares with the event's goods and donations — so a caller rebuilding the page can tell that a t-shirt sits between two tickets rather than after all of them. Lower comes first. The list is already returned in this order; the number is here because array order alone cannot say how a ticket relates to a good.
ticket_types[].section_idinteger- The section of the event page this type renders under — one of the sections listed on GET /events/{slug}. This is where the page actually puts it, not the raw stored column: a type nobody has placed by hand falls to the section that claims its kind.
ticket_types[].companion.ticket_type_idinteger- The companion — the ticket type this rule asks for.
ticket_types[].companion.quantityinteger- How many companions are needed per
per_quantityof this type. ticket_types[].companion.strictnessstringrecommendedshows people a tip and never blocks;requiredrefuses a checkout that falls short — unless the companion is sold out, in which case the rule pauses so this type stays sellable.- one of
recommendedrequired
Responses
200The event's ticket types, sorted by their configured display order.ticket_typesis always[], never null, when the event has none (including an unknown slug, which matches nothing rather than 404ing).401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"ticket_types": [
{
"id": 1,
"name": "Early Bird",
"price_cents": 14900,
"currency": "eur",
"prices": {
"eur": 14900,
"gbp": 12900
},
"on_sale": true,
"quantity": 200,
"sold": 142,
"tiers": [
{
"id": 1,
"name": "First 100",
"price_cents": 12900,
"quantity": 100,
"sold": 100,
"starts_at": null,
"ends_at": "2026-06-01T00:00:00Z",
"live": false,
"prices": {
"eur": 14900,
"gbp": 12900
}
}
],
"bands": [
{
"from_qty": 3,
"price_cents": 8000,
"prices": {
"eur": 8000,
"gbp": 6900
}
}
],
"position": 1,
"section_id": 1,
"companion": {
"ticket_type_id": 2,
"quantity": 1,
"per_quantity": 1,
"strictness": "recommended"
}
}
]
}
List series
GET /series
Requires events.view
Response fields
seriesarray of Seriesseries[].slugstring- The address the series page answers on. Unique across this account's events, series and custom pages.
series[].namestringseries[].listedboolean- Whether the series appears on the account's own front page. It keeps its address either way.
series[].created_atstring (RFC 3339) or null
Responses
200Series, by name.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.
curl https://<yourslug>.go.tito.io/api/v1/series \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/series", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"series": [
{
"slug": "annual-conference",
"name": "Annual Conference",
"intro": "Every autumn since 2019.",
"listed": true,
"show_past": true,
"event_count": 7,
"created_at": "2019-03-02T11:20:41Z"
}
]
}
Orders
An order is one purchase: a buyer, an amount, and the lines they bought. Refs are account-wide. Machine checkout creates one the same way the shop does.
List orders
GET /events/{slug}/orders
List an event's orders (paged, newest first).
Requires orders.view
Path parameters
slugstring required- The event's slug.
Query parameters
afterstring- Cursor: the ref returned as the previous page's
next_after. Omit for the first page. limitinteger- Page size. Default 50, max 200. Non-positive or unparseable values fall back to the default.
Response fields
ordersarray of Orderorders[].refstring- Short reference printed on receipts and read out at the desk. Unique across the account.
orders[].legacy_refstring- The ref this order carried on the platform it was migrated from; empty for orders sold on Tito. It is not always the same as
ref: refs are unique per account here and were unique per event on the source, so a migrated order whose code was already in use was renumbered on import. This field is what the buyer's original confirmation says, and what an integration keyed on the old platform reconciles against. orders[].statusstring- The order's state. Pending orders hold stock until paid or expired.
- one of
pendingpaidcanceledrefunded orders[].created_atstring (RFC 3339) or null- When the order was started; for a pending order, when checkout began.
orders[].itemsarray of OrderItem- The order's TICKET seats, in insertion order — not its whole contents; see
lines. Always an array, never null — an order with no ticket items serializes as[]. orders[].items[].ticket_typestring- The ticket type's name, joined in so the raw ticket_type id is never exposed.
orders[].linesarray of OrderLine- The order's whole priced ledger, in the order it was written: tickets, goods, donations and post-order adjustments. The active lines'
total_centssum to what the order was charged BEFORE any promotion code came off it — a redeemed code never touches a line, and is reported indiscountsinstead. Subtract those to reachamount_cents. Overlapsitemson tickets by design: a ticket seat appears in both. Always an array, never null. orders[].lines[].product_idinteger- The catalog row this line sold. The same handle
item_answersis keyed on, so what the buyer said about each of these joins to the line that sold them. 0 when no product row backs the line (a ticket type predating the product backfill, whose seats are read out of order_items instead). orders[].lines[].productstring- The product's name. A ticket line carries the live ticket type name, so it agrees with the matching
itemsentry. orders[].lines[].kindstring- The product's kind: "ticket", "item" or "donation".
- one of
ticketitemdonation orders[].lines[].quantityintegerorders[].lines[].tax_centsinteger- Integer cents. The tax snapshot taken when the line was written; 0 when the line carries no tax.
orders[].lines[].total_centsinteger- Integer cents. Subtotal plus an exclusive rate's tax; the subtotal alone for an inclusive one.
orders[].lines[].statusstring- "active", "removed" or "refunded". Only active lines count towards money; removed and refunded lines are reported rather than dropped, so an order can be reconciled.
- one of
activeremovedrefunded orders[].lines[].tier_idinteger- The price tier this line was SOLD at. Absent when the ticket has no price ladder. It is reported because a caller cannot work it out after the fact: which rung was live is decided by the clock and by what that rung had left, and by the time an old order is read the ladder has moved on. A tier the organizer removed after it had sold tickets is hidden from the ticket type's
tiers, so a line can name a tier that is no longer listed there — the organizer's Sales report is where it is still named. orders[].lines[].band_idinteger- The volume band this line was CHARGED at. Absent both when the ticket has no volume ladder and when the buyer paid the ticket's own price — the ladder's first rung, which has no band of its own. It is reported rather than left to be re-derived because a ladder is editable: the band that priced these seats may not exist any more, and resolving
quantityagainst today's ladder would name a rung the buyer was never charged at. Matches an entry of the ticket type'sbandswhile the band is still on sale. orders[].lines[].ticket_refstring- The ticket this line hangs off — an upgrade, or something bought for one person after the fact. Absent when the line hangs off no ticket, which is every ordinary ticket line.
orders[].discountsarray of OrderDiscount- The promotion codes redeemed against this order, in the order they were applied — what came OFF it, where
linesis what went on. This is one of the two terms that make the total reconcilable, alongsideprice_adjustments. Almost always empty; always an array, never null. orders[].discounts[].codestring- The promotion code as the buyer typed it, recorded on the redemption — so it still reads back after the code itself has been renamed or deleted.
orders[].discounts[].amount_centsinteger- What this code took off the order, in integer minor units. POSITIVE, and subtracted: the buyer's own receipt renders it as a negative row, while the API reports the amount as the redemption holds it.
orders[].price_adjustmentsarray of OrderPriceAdjustment- A price the organizer set for this whole order, if they set one. Signed and added, so the full identity is: the active
linestotals, less thediscountsamount_cents, plus theseamount_cents, equal the order'samount_cents. Empty on every order nobody hand-priced, which is almost all of them; always an array, never null. orders[].price_adjustments[].labelstring- What the organizer called the difference, for example "Sponsor rate". This is the text the buyer reads beside the amount on their payment page and their receipt.
orders[].price_adjustments[].amount_centsinteger- The difference against the order's lines, in integer minor units. SIGNED and added: negative took money off the order, positive added to it.
next_afterstring or null- Order ref to pass as
?after=for the next page; null when there are no further pages; a full final page yields a cursor to an empty page.
Responses
200A page of orders, each with its ticket seats (items), its whole priced ledger (lines) and the promotion codes redeemed against it (discounts).ordersis always[], never null, when the event has none.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/orders \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/orders", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"orders": [
{
"ref": "H7K2PX",
"legacy_ref": "",
"status": "paid",
"email": "ada@example.com",
"name": "Ada Lovelace",
"amount_cents": 29800,
"currency": "eur",
"created_at": "2026-05-18T14:02:11Z",
"paid_at": "2026-05-18T14:03:07Z",
"refunded_at": null,
"items": [
{
"ticket_type": "Early Bird",
"quantity": 2,
"unit_price_cents": 14900
}
],
"lines": [
{
"product_id": 1,
"product": "Early Bird",
"kind": "ticket",
"quantity": 2,
"unit_price_cents": 14900,
"subtotal_cents": 29800,
"tax_cents": 0,
"total_cents": 29800,
"currency": "eur",
"status": "active",
"tier_id": 0,
"band_id": 0,
"ticket_ref": ""
}
],
"discounts": [
{
"code": "EARLYBIRD",
"amount_cents": 2000,
"currency": "eur"
}
],
"price_adjustments": [
{
"label": "Speaker discount",
"amount_cents": -5000,
"currency": "eur"
}
]
}
],
"next_after": "example"
}
Create an order
POST /events/{slug}/orders
Create an order (machine checkout). Creates an order through the same pipeline the shop's own checkout runs: the ticket types must be on sale on this event's main page, the per-order quantity cap and the availability check are the shop's, and the total is priced by the shop's own pricing core (group discounts and tax included). A payable order is created pending and answers with payment_url, the buyer's own pay page. Nothing is charged here and no Stripe call is made, so an account whose Stripe key is not yet configured can still create orders. A zero-total order has nothing to pay, so it is fulfilled immediately (tickets minted, ticket email sent) and answers status: "paid" with url, the buyer's order page; because it is fulfilled on the spot, email is required for that case only.
A pending order holds its stock for as long as it can still be paid. The shop's own checkout is released by Stripe's session-expired webhook, and an order created here has no session until the buyer opens payment_url, so its hold instead runs to the event's own payment window — the organizer's configured expiry, or (the default) a day past the event start, and never less than a day away. That is the same hold an admin-issued payment link takes. Cancel abandoned orders with POST /orders/{ref}/cancel to release their places early — create orders when a buyer is real, and cancel any you strand.
Goods included with a ticket come too. An offering an organizer set to Included on a ticket type is not something a caller asks for, so it is not in items: it is added for you, one per connected ticket, as a zero-price line, and it holds that good's stock exactly as a bought one does. The order this endpoint creates therefore matches the order the same selection would have produced on the event page. A good that has run out refuses the whole create with 409 sold_out naming it, and nothing is written.
What this endpoint does not collect. Questions — including required ones — are not asked for here: the order arrives with no answers, and the organizer chases them (or the buyer fills them in on the order page). Ticket types whose product carries a scheduled entitlement cannot be sold this way at all, because a session would have to be chosen; they answer 422 invalid_items naming the type. Nor can audience-only types: the API sells what the event's main page sells.
payment_url and url are the ONE documented exception to the rule that buyer capability URLs never appear in API responses: handing the buyer their pay link is the whole product of this endpoint. Treat both as secrets — whoever holds the URL holds the order.
Send Idempotency-Key to make retries safe: the first call creates the order, and every later call with the same key answers 200 (not 201) with that same order's current state — a still-pending order returns its payment_url again, an order that has since been paid returns status: "paid" and url. The body is not compared against the original; the key alone identifies the order, and racing calls with one key resolve to one order. Without the header there is no deduplication and every call creates a new order. Scoped to the API key that sends it: two keys reusing the same value get independent orders.
Requires orders.manage
Path parameters
slugstring required- The event's slug.
Query parameters
Idempotency-Keystring- Optional caller-chosen key, at most 200 characters, that makes this create safe to retry. Reusing a key returns the order it already created (200), without holding a second lot of stock. Scoped to the API key that sends it: two keys reusing the same value get independent orders.
Request body
items[].ticket_type_idinteger required- A ticket type id from GET /events/{slug}/ticket-types. It must belong to this event and be on sale.
items[].quantityinteger required- How many tickets of this type — capped by the type's own max_per_order when it has one, and by what is left. A type that sets no cap of its own has none on this endpoint.
emailstring- Buyer email. Required only for a zero-total order, which is fulfilled immediately; on a payable order Stripe collects it at checkout, so it is optional here.
Response fields
statusstring- "pending" for a payable order awaiting payment, "paid" for a zero-total order (fulfilled on creation). A replay reports whatever the order is now, canceled or refunded included.
- one of
pendingpaidcanceledrefunded payment_urlstring- Absolute URL of the buyer's pay page. Present only while the order is pending. A capability URL: treat it as a secret.
urlstring- Absolute URL of the buyer's order page. Present once the order is no longer pending (a free order, or a replay of one since paid). A capability URL: treat it as a secret.
Response fields
statusstring- "pending" for a payable order awaiting payment, "paid" for a zero-total order (fulfilled on creation). A replay reports whatever the order is now, canceled or refunded included.
- one of
pendingpaidcanceledrefunded payment_urlstring- Absolute URL of the buyer's pay page. Present only while the order is pending. A capability URL: treat it as a secret.
urlstring- Absolute URL of the buyer's order page. Present once the order is no longer pending (a free order, or a replay of one since paid). A capability URL: treat it as a secret.
Responses
201The order was created — pending withpayment_urlwhen it is payable, paid withurlwhen it is free (its tickets already exist).200A replay: this Idempotency-Key already created an order, and this is that order's current state. Nothing was created and no extra stock is held.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409sold_out— not enough left of one of the requested ticket types, or of a good included with one. Two checks produce it: the availability read the shop's own event page uses, which names the type (or the included good) and how many remain, and the capacity check inside the write transaction, which is the one that cannot be raced and reports only that something sold out mid-create. Re-read the ticket types and retry.422The request cannot become an order:invalid_body(not JSON, a wrong field type, an explicit null, or an unknown field),invalid_idempotency_key(the header is longer than 200 characters),invalid_items(no items, a quantity below 1, a repeated ticket type, a quantity over the type's per-order cap, a type that is unknown, belongs to another event or is not on sale, or a type that needs a session chosen at checkout — the message names it),sales_closed(sales for this event have ended),email_required(a zero-total order is fulfilled immediately and needs somewhere to send the tickets), oramount_too_small(the total is below the smallest amount a card can be charged).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/orders \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"items":[{"ticket_type_id":1,"quantity":2}]}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/orders", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"items": [
{
"ticket_type_id": 1,
"quantity": 2
}
]
}),
});
const data = await res.json();
The order was created — pending with payment_url when it is payable, paid with url when it is free (its tickets already exist).
{
"ref": "H7K2PX",
"status": "pending",
"total_cents": 29800,
"currency": "eur",
"payment_url": "https://annual-conf.example/annual-conf/orders/…/pay",
"url": "https://annual-conf.example/annual-conf/orders/…"
}
A replay: this Idempotency-Key already created an order, and this is that order's current state.
{
"ref": "H7K2PX",
"status": "pending",
"total_cents": 29800,
"currency": "eur",
"payment_url": "https://annual-conf.example/annual-conf/orders/…/pay",
"url": "https://annual-conf.example/annual-conf/orders/…"
}
Get an order
GET /orders/{ref}
Get an order by ref, account-wide (refs are globally unique).
Requires orders.view
Path parameters
refstring required- The order's ref (account-wide, not scoped to an event).
Response fields
refstring- Short reference printed on receipts and read out at the desk. Unique across the account.
legacy_refstring- The ref this order carried on the platform it was migrated from; empty for orders sold on Tito. It is not always the same as
ref: refs are unique per account here and were unique per event on the source, so a migrated order whose code was already in use was renumbered on import. This field is what the buyer's original confirmation says, and what an integration keyed on the old platform reconciles against. statusstring- The order's state. Pending orders hold stock until paid or expired.
- one of
pendingpaidcanceledrefunded created_atstring (RFC 3339) or null- When the order was started; for a pending order, when checkout began.
itemsarray of OrderItem- The order's TICKET seats, in insertion order — not its whole contents; see
lines. Always an array, never null — an order with no ticket items serializes as[]. items[].ticket_typestring- The ticket type's name, joined in so the raw ticket_type id is never exposed.
linesarray of OrderLine- The order's whole priced ledger, in the order it was written: tickets, goods, donations and post-order adjustments. The active lines'
total_centssum to what the order was charged BEFORE any promotion code came off it — a redeemed code never touches a line, and is reported indiscountsinstead. Subtract those to reachamount_cents. Overlapsitemson tickets by design: a ticket seat appears in both. Always an array, never null. lines[].product_idinteger- The catalog row this line sold. The same handle
item_answersis keyed on, so what the buyer said about each of these joins to the line that sold them. 0 when no product row backs the line (a ticket type predating the product backfill, whose seats are read out of order_items instead). lines[].productstring- The product's name. A ticket line carries the live ticket type name, so it agrees with the matching
itemsentry. lines[].quantityintegerlines[].tax_centsinteger- Integer cents. The tax snapshot taken when the line was written; 0 when the line carries no tax.
lines[].total_centsinteger- Integer cents. Subtotal plus an exclusive rate's tax; the subtotal alone for an inclusive one.
lines[].statusstring- "active", "removed" or "refunded". Only active lines count towards money; removed and refunded lines are reported rather than dropped, so an order can be reconciled.
- one of
activeremovedrefunded lines[].tier_idinteger- The price tier this line was SOLD at. Absent when the ticket has no price ladder. It is reported because a caller cannot work it out after the fact: which rung was live is decided by the clock and by what that rung had left, and by the time an old order is read the ladder has moved on. A tier the organizer removed after it had sold tickets is hidden from the ticket type's
tiers, so a line can name a tier that is no longer listed there — the organizer's Sales report is where it is still named. lines[].band_idinteger- The volume band this line was CHARGED at. Absent both when the ticket has no volume ladder and when the buyer paid the ticket's own price — the ladder's first rung, which has no band of its own. It is reported rather than left to be re-derived because a ladder is editable: the band that priced these seats may not exist any more, and resolving
quantityagainst today's ladder would name a rung the buyer was never charged at. Matches an entry of the ticket type'sbandswhile the band is still on sale. lines[].ticket_refstring- The ticket this line hangs off — an upgrade, or something bought for one person after the fact. Absent when the line hangs off no ticket, which is every ordinary ticket line.
discountsarray of OrderDiscount- The promotion codes redeemed against this order, in the order they were applied — what came OFF it, where
linesis what went on. This is one of the two terms that make the total reconcilable, alongsideprice_adjustments. Almost always empty; always an array, never null. discounts[].codestring- The promotion code as the buyer typed it, recorded on the redemption — so it still reads back after the code itself has been renamed or deleted.
discounts[].amount_centsinteger- What this code took off the order, in integer minor units. POSITIVE, and subtracted: the buyer's own receipt renders it as a negative row, while the API reports the amount as the redemption holds it.
price_adjustmentsarray of OrderPriceAdjustment- A price the organizer set for this whole order, if they set one. Signed and added, so the full identity is: the active
linestotals, less thediscountsamount_cents, plus theseamount_cents, equal the order'samount_cents. Empty on every order nobody hand-priced, which is almost all of them; always an array, never null. price_adjustments[].labelstring- What the organizer called the difference, for example "Sponsor rate". This is the text the buyer reads beside the amount on their payment page and their receipt.
price_adjustments[].amount_centsinteger- The difference against the order's lines, in integer minor units. SIGNED and added: negative took money off the order, positive added to it.
ticketsarray of string- Ticket refs belonging to this order. Empty array, never null, when the order produced none (e.g. still pending).
answersarray of Answer- Answers to the questions asked on the order form itself, once for the whole order. Always an array, never null. Includes any metadata the organizer records against the order and keeps to themselves — this read is gated by orders.view, the very capability that gates the admin's own order page. Per-attendee answers are not here: they belong to the ticket, and the ticket reads carry them.
answers[].field_idintegeranswers[].keystring- The stable machine handle: unique, and fixed when the question or detail was created. Match on this rather than on
label, which the organizer may rewrite any afternoon. answers[].labelstring- The question, or the name of the detail, as the organizer wrote it, in the account's own language. Per-locale overlays are for people reading a page; a response has no reader whose language it could mean.
answers[].kindstring- How it was asked, and so how to read
value: "text", "textarea", "select", "multi_select", "checkbox", "yes_no", "number", "date", "phone" or "file". answers[].valuestring- The answer exactly as stored, never as displayed: a yes/no reads "yes" or "no" rather than the word a page would show it as, a ticked checkbox reads "yes", a multi-select is its choices as one comma-separated list, and a phone number is the digits as given. A question left blank is omitted from the list rather than reported as an empty answer. On a "file" question this names what was uploaded; the bytes themselves are not served by the API.
item_answersarray of ItemAnswers- What the buyer said about each individual thing they bought — one entry per (product, position), and only for the ones something was answered about. Always an array, never null.
item_answers[].product_idintegeritem_answers[].positioninteger- Which of this product's units this is: zero-based, and counted per PRODUCT across the whole order rather than per line item, because a good split across price bands is still one list of t-shirts to the person buying them. Position 0 is the one the buyer's own order page labels "T-shirt 1".
item_answers[].answersarray of Answer- This item's answers, in a stable order. Never empty — an item with nothing answered about it has no entry at all.
item_answers[].answers[].field_idintegeritem_answers[].answers[].keystring- The stable machine handle: unique, and fixed when the question or detail was created. Match on this rather than on
label, which the organizer may rewrite any afternoon. item_answers[].answers[].labelstring- The question, or the name of the detail, as the organizer wrote it, in the account's own language. Per-locale overlays are for people reading a page; a response has no reader whose language it could mean.
item_answers[].answers[].kindstring- How it was asked, and so how to read
value: "text", "textarea", "select", "multi_select", "checkbox", "yes_no", "number", "date", "phone" or "file". item_answers[].answers[].valuestring- The answer exactly as stored, never as displayed: a yes/no reads "yes" or "no" rather than the word a page would show it as, a ticked checkbox reads "yes", a multi-select is its choices as one comma-separated list, and a phone number is the digits as given. A question left blank is omitted from the list rather than reported as an empty answer. On a "file" question this names what was uploaded; the bytes themselves are not served by the API.
Responses
200The order: its ticket seats (items), its whole priced ledger (lines— the goods and donations too), the promotion codes redeemed against it (discounts), the refs of the tickets it produced, and what the buyer told us, both the order form's own answers and what they said about each thing they bought. Never includes the buyer's order-page token (a URL capability, not API data).401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"ref": "H7K2PX",
"legacy_ref": "",
"status": "paid",
"email": "ada@example.com",
"name": "Ada Lovelace",
"amount_cents": 29800,
"currency": "eur",
"created_at": "2026-05-18T14:02:11Z",
"paid_at": "2026-05-18T14:03:07Z",
"refunded_at": null,
"items": [
{
"ticket_type": "Early Bird",
"quantity": 2,
"unit_price_cents": 14900
}
],
"lines": [
{
"product_id": 1,
"product": "Early Bird",
"kind": "ticket",
"quantity": 2,
"unit_price_cents": 14900,
"subtotal_cents": 29800,
"tax_cents": 0,
"total_cents": 29800,
"currency": "eur",
"status": "active",
"tier_id": 0,
"band_id": 0,
"ticket_ref": ""
}
],
"discounts": [
{
"code": "EARLYBIRD",
"amount_cents": 2000,
"currency": "eur"
}
],
"price_adjustments": [
{
"label": "Speaker discount",
"amount_cents": -5000,
"currency": "eur"
}
],
"tickets": [
"T9QW4M"
],
"answers": [
{
"field_id": 3,
"key": "dietary",
"label": "Dietary requirements",
"kind": "text",
"value": "Vegetarian"
}
],
"item_answers": [
{
"product_id": 1,
"product": "Early Bird",
"position": 1,
"answers": [
{
"field_id": 3,
"key": "dietary",
"label": "Dietary requirements",
"kind": "text",
"value": "Vegetarian"
}
]
}
]
}
Update an order's buyer
PATCH /orders/{ref}
Correct the name and email on an order. Corrects the name and email address the ORDER carries — where its confirmation, receipt and invoice go. This is not a ticket's attendee: moving a ticket to a different person is PATCH /tickets/{ref}, which reassigns it, rotates its link and emails both people. Nothing is emailed here. The organizer's own form offers to re-send the order link to the corrected address; an API caller decides that for itself, so this call only writes. Absent fields are left unchanged; an explicit null is refused, and so is an unknown field. email is checked for syntax only, the same bargain PATCH /tickets/{ref} makes. Links already sent to the old address keep working unless revoke_link is true. Answers the order in the read shape.
Requires orders.manage
Path parameters
refstring required- The order's ref (account-wide, not scoped to an event).
Request body
revoke_linkboolean- Take the order away from the address it is moving off: the order's own link is replaced, and so is the link of every ticket that address was sent (a ticket belonging to somebody else's address keeps its own — naming a holder already rotated it). Ignored unless
emailactually changes the address. Nothing is emailed, so the buyer is left without a working link until you send them one.
Response fields
refstring- Short reference printed on receipts and read out at the desk. Unique across the account.
legacy_refstring- The ref this order carried on the platform it was migrated from; empty for orders sold on Tito. It is not always the same as
ref: refs are unique per account here and were unique per event on the source, so a migrated order whose code was already in use was renumbered on import. This field is what the buyer's original confirmation says, and what an integration keyed on the old platform reconciles against. statusstring- The order's state. Pending orders hold stock until paid or expired.
- one of
pendingpaidcanceledrefunded created_atstring (RFC 3339) or null- When the order was started; for a pending order, when checkout began.
itemsarray of OrderItem- The order's TICKET seats, in insertion order — not its whole contents; see
lines. Always an array, never null — an order with no ticket items serializes as[]. items[].ticket_typestring- The ticket type's name, joined in so the raw ticket_type id is never exposed.
linesarray of OrderLine- The order's whole priced ledger, in the order it was written: tickets, goods, donations and post-order adjustments. The active lines'
total_centssum to what the order was charged BEFORE any promotion code came off it — a redeemed code never touches a line, and is reported indiscountsinstead. Subtract those to reachamount_cents. Overlapsitemson tickets by design: a ticket seat appears in both. Always an array, never null. lines[].product_idinteger- The catalog row this line sold. The same handle
item_answersis keyed on, so what the buyer said about each of these joins to the line that sold them. 0 when no product row backs the line (a ticket type predating the product backfill, whose seats are read out of order_items instead). lines[].productstring- The product's name. A ticket line carries the live ticket type name, so it agrees with the matching
itemsentry. lines[].quantityintegerlines[].tax_centsinteger- Integer cents. The tax snapshot taken when the line was written; 0 when the line carries no tax.
lines[].total_centsinteger- Integer cents. Subtotal plus an exclusive rate's tax; the subtotal alone for an inclusive one.
lines[].statusstring- "active", "removed" or "refunded". Only active lines count towards money; removed and refunded lines are reported rather than dropped, so an order can be reconciled.
- one of
activeremovedrefunded lines[].tier_idinteger- The price tier this line was SOLD at. Absent when the ticket has no price ladder. It is reported because a caller cannot work it out after the fact: which rung was live is decided by the clock and by what that rung had left, and by the time an old order is read the ladder has moved on. A tier the organizer removed after it had sold tickets is hidden from the ticket type's
tiers, so a line can name a tier that is no longer listed there — the organizer's Sales report is where it is still named. lines[].band_idinteger- The volume band this line was CHARGED at. Absent both when the ticket has no volume ladder and when the buyer paid the ticket's own price — the ladder's first rung, which has no band of its own. It is reported rather than left to be re-derived because a ladder is editable: the band that priced these seats may not exist any more, and resolving
quantityagainst today's ladder would name a rung the buyer was never charged at. Matches an entry of the ticket type'sbandswhile the band is still on sale. lines[].ticket_refstring- The ticket this line hangs off — an upgrade, or something bought for one person after the fact. Absent when the line hangs off no ticket, which is every ordinary ticket line.
discountsarray of OrderDiscount- The promotion codes redeemed against this order, in the order they were applied — what came OFF it, where
linesis what went on. This is one of the two terms that make the total reconcilable, alongsideprice_adjustments. Almost always empty; always an array, never null. discounts[].codestring- The promotion code as the buyer typed it, recorded on the redemption — so it still reads back after the code itself has been renamed or deleted.
discounts[].amount_centsinteger- What this code took off the order, in integer minor units. POSITIVE, and subtracted: the buyer's own receipt renders it as a negative row, while the API reports the amount as the redemption holds it.
price_adjustmentsarray of OrderPriceAdjustment- A price the organizer set for this whole order, if they set one. Signed and added, so the full identity is: the active
linestotals, less thediscountsamount_cents, plus theseamount_cents, equal the order'samount_cents. Empty on every order nobody hand-priced, which is almost all of them; always an array, never null. price_adjustments[].labelstring- What the organizer called the difference, for example "Sponsor rate". This is the text the buyer reads beside the amount on their payment page and their receipt.
price_adjustments[].amount_centsinteger- The difference against the order's lines, in integer minor units. SIGNED and added: negative took money off the order, positive added to it.
Responses
200The order, in the same shapeGET /orders/{ref}answers with.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body was unreadable, carried an unknown field or an explicit null, blanked the name, or the email isn't an email address (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Ada Lovelace"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Ada Lovelace"
}),
});
const data = await res.json();
{
"ref": "H7K2PX",
"legacy_ref": "",
"status": "paid",
"email": "ada@example.com",
"name": "Ada Lovelace",
"amount_cents": 29800,
"currency": "eur",
"created_at": "2026-05-18T14:02:11Z",
"paid_at": "2026-05-18T14:03:07Z",
"refunded_at": null,
"items": [
{
"ticket_type": "Early Bird",
"quantity": 2,
"unit_price_cents": 14900
}
],
"lines": [
{
"product_id": 1,
"product": "Early Bird",
"kind": "ticket",
"quantity": 2,
"unit_price_cents": 14900,
"subtotal_cents": 29800,
"tax_cents": 0,
"total_cents": 29800,
"currency": "eur",
"status": "active",
"tier_id": 0,
"band_id": 0,
"ticket_ref": ""
}
],
"discounts": [
{
"code": "EARLYBIRD",
"amount_cents": 2000,
"currency": "eur"
}
],
"price_adjustments": [
{
"label": "Speaker discount",
"amount_cents": -5000,
"currency": "eur"
}
]
}
Cancel an order
POST /orders/{ref}/cancel
Cancel a pending order. Cancels a still-pending order and releases the stock it holds — the API's copy of the admin's payment-link cancel. Only a pending order is cancelable; this exists because an API-created pending order holds stock for the event's whole payment window with no Stripe session to expire it early, unlike the browser checkout's 35-minute hold. Send no request body; any body is ignored. A door (card-reader) sale's pending order backs a live card-present PaymentIntent: that intent is voided at Stripe before the order is released, and if Stripe cannot prove the charge is off the cancel is refused with cancel_failed (502) and nothing changes. Known exposure, recorded deliberately: a buyer who already opened payment_url holds a live Stripe session that this call does not expire — they can still complete that payment after the order is canceled locally.
Requires orders.manage
Path parameters
refstring required- The order's ref (account-wide, not scoped to an event).
Response fields
Responses
200The order is canceled and its stock released.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The order is not pending — it is paid, refunded, or already canceled (not_pending).500Internal error.502The card payment behind the order could not be voided at Stripe (cancel_failed) — nothing was changed; retry, or resolve the payment from the Stripe dashboard.
curl -X POST https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/cancel \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/cancel", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"canceled": true,
"ref": "H7K2PX"
}
Mark an order paid
POST /orders/{ref}/mark-paid
Record an off-rail payment against a pending order. Records a payment taken outside Tito — a cheque, cash, a bank transfer the organizer watched arrive — against a still-pending order, and fulfils it through the same core a card payment runs: the tickets are issued, the buyer's confirmation email goes out, workflows fire. Send no request body; any body is ignored. Nothing is recorded as collected, because no money moved on a rail Tito can see: no payments row is written and no payment id is stamped, which is the same shape a complimentary/paid-offline order has — so the order can be canceled afterwards but never refunded through Tito. An order awaiting a bank transfer is the one exception: its invoice is marked funded and it keeps the payment it already names. Whatever could still charge the order (an open Checkout Session, a card-present intent on a reader, the virtual IBAN behind an invoice) is retired first, best-effort. Only a pending order can be marked paid; anything else answers 409 not_pending.
Requires orders.manage
Path parameters
refstring required- The order's ref (account-wide, not scoped to an event).
Response fields
Responses
200The order is paid and its tickets are issued.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The order is not pending — it is paid, refunded, or canceled (not_pending).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/mark-paid \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/mark-paid", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"paid": true,
"ref": "H7K2PX"
}
Refund an order
POST /orders/{ref}/refund
Refund a whole paid order. Refunds the order's remaining balance through the same core the admin's refund button uses: the order's own rail first (Stripe, PayPal, or a sandbox's simulated payment), then the local flip (order refunded, its tickets voided, stock released, a refund row in the payments ledger, the buyer's refund email). Money moves — the key must carry the standalone refund capability, which no other capability implies. Send no request body; any body is ignored. Idempotent: an order that is already refunded answers 200 with already: true and never reaches its provider again. An order in any other status (pending, canceled) answers 422 not_refundable — canceling a pending order is a different verb this API does not have yet. A paid order whose balance has already come back in full (every ticket refunded one by one) answers 422 not_refundable too: there is nothing left to refund, so this is a refusal and not a failure to retry. A paid order with no recorded payment on any rail (a free/zero-cent order, a complimentary one, or one whose reference was never persisted) answers 422 no_payment_intent, the same refusal the admin makes — and that check comes first, so a free order on an account with no payments configured gets that 422 rather than a 503 about payments it never needed. refunded_cents is the order total less any refunds already made against it (ticket or line refunds) — exactly what the provider moves and what the ledger records. Inside an account sandbox a simulated payment refunds the same way and no processor is called: the refund is simulated too, and the ledger says so.
Requires refund
Path parameters
refstring required- The order's ref (account-wide, not scoped to an event).
Response fields
alreadyboolean- Present and true only when the refund had already happened before this call — the replay-safe case. No money moved and no provider call was made;
refunded_centsis omitted, since this call refunded nothing. refunded_centsinteger- Integer cents this call refunded (0 for a free/zero-cent ticket). Omitted when
alreadyis true.
Responses
200The order is refunded — either just now (withrefunded_cents) or already (already: true), which is a success, not an error.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The order is not in a refundable state: it is pending or canceled, or its balance is already fully refunded (not_refundable); or it is paid but carries no payment to refund against (no_payment_intent).500Internal error.502The refund did not complete (refund_failed): either the payment provider refused it — in which case nothing moved and nothing changed locally — or it succeeded and the local write that follows it failed, which the instance journals as CRITICAL. The two are not distinguishable from here, and re-reading the resource does not separate them either (after the second case the order still readspaid). Retry: it is safe, and it is the repair. The provider idempotency key is derived from the order ref, so a retry replays the original refund rather than moving money twice, and the local write that failed gets another run. Left alone, the provider's own refund webhook converges the local state anyway.503Payments are not configured on this account, so no refund can be sent (payments_unavailable).
curl -X POST https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/refund \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/refund", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"refunded": true,
"already": true,
"refunded_cents": 14900
}
Tickets
A ticket is one admission produced by a paid order, held by an attendee who may differ from the buyer.
List tickets
GET /events/{slug}/tickets
List an event's tickets/attendees (paged, newest first).
Requires attendees.view
Path parameters
slugstring required- The event's slug.
Query parameters
afterstring- Cursor: the ref returned as the previous page's
next_after. Omit for the first page. limitinteger- Page size. Default 50, max 200. Non-positive or unparseable values fall back to the default.
Response fields
ticketsarray of Tickettickets[].legacy_refstring- The ref this ticket carried on the platform it was migrated from; empty for tickets sold on Tito. See the same field on Order.
tickets[].answersarray of Answer- What this ticket's holder was asked, plus any metadata the organizer records against them. Always an array, never null — a ticket with neither serializes as
[]. Includes the metadata the organizer keeps to their own team: this read is gated by attendees.view, the very capability that gates the admin's own attendee page.A question can be asked because of the ticket type OR because of a session the ticket got this person into. An organizer can attach a question to a session offering and narrow it to some of its options, so 'what's your level of Rust?' is asked only of the people who picked the Rust workshop. Those answers arrive here, in this same array and this same shape — there is no separate field and nothing says which of the two asked it. Two holders of the same ticket type can therefore carry different questions, and moving somebody to another option drops the answers the new option does not ask for.
tickets[].answers[].field_idintegertickets[].answers[].keystring- The stable machine handle: unique, and fixed when the question or detail was created. Match on this rather than on
label, which the organizer may rewrite any afternoon. tickets[].answers[].labelstring- The question, or the name of the detail, as the organizer wrote it, in the account's own language. Per-locale overlays are for people reading a page; a response has no reader whose language it could mean.
tickets[].answers[].kindstring- How it was asked, and so how to read
value: "text", "textarea", "select", "multi_select", "checkbox", "yes_no", "number", "date", "phone" or "file". tickets[].answers[].valuestring- The answer exactly as stored, never as displayed: a yes/no reads "yes" or "no" rather than the word a page would show it as, a ticked checkbox reads "yes", a multi-select is its choices as one comma-separated list, and a phone number is the digits as given. A question left blank is omitted from the list rather than reported as an empty answer. On a "file" question this names what was uploaded; the bytes themselves are not served by the API.
tickets[].syncarray of SyncStanding- Where this ticket stands with each connected service. Always an array, never null — a ticket never sent to any service serializes as
[]. tickets[].sync[].remote_idstring- The id the service knows this ticket by. Empty when nothing has synced yet.
tickets[].sync[].synced_atstring (RFC 3339) or null- When the last successful sync completed; null when it has never succeeded.
tickets[].sync[].errorstring- The service's own words for its last failure, verbatim. Empty when the last attempt succeeded.
next_afterstring or null- Ticket ref to pass as
?after=for the next page; null when there are no further pages; a full final page yields a cursor to an empty page.
Responses
200A page of tickets.ticketsis always[], never null, when the event has none.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/tickets \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/tickets", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"tickets": [
{
"ref": "T9QW4M",
"legacy_ref": "",
"status": "valid",
"ticket_type": "Early Bird",
"holder_name": "Ada Lovelace",
"holder_email": "ada@example.com",
"order_ref": "H7K2PX",
"created_at": "2026-05-18T14:03:07Z",
"answers": [
{
"field_id": 3,
"key": "dietary",
"label": "Dietary requirements",
"kind": "text",
"value": "Vegetarian"
}
],
"sync": [
{
"service": "example",
"remote_id": "example",
"state": "synced",
"synced_at": "2026-10-14T09:00:00Z",
"error": "example"
}
]
}
],
"next_after": "example"
}
Update a ticket's holder
PATCH /tickets/{ref}
Update a ticket's holder (name and/or email). Changes who a ticket is for. Both body fields are optional and independent: a field absent from the JSON is left unchanged, which is not the same as sending it empty. Changing holder_email rotates the ticket's token — the previous holder's ticket link stops working — and clears the assignment-notified stamp, exactly as the admin's attendee edit does (shared code). Unlike the admin edit, this endpoint sends no email (neither to the previous holder nor to the new one), writes no audit row, and does not touch the ticket's custom-field answers. A void ticket, or a ticket of an anonymous type (which carries no holder by design), answers 422 not_editable.
Requires orders.manage
Path parameters
refstring required- The ticket's ref (account-wide, not scoped to an event), matched against the ticket's own ref — an order ref is not a ticket ref, and answers 404 here. The ticket's token — the buyer's URL capability — is never accepted here and never returned.
Request body
holder_namestring- The holder's full name. Omit to leave it unchanged (an explicit null is rejected, not treated as unchanged). Trimmed, capped at 200 characters, and reparsed into the ticket's stored name parts. A blank or whitespace-only value is rejected (422
invalid_body) — the admin's form refuses it too. holder_emailstring- The holder's email address. Omit to leave it unchanged (an explicit null is rejected, not treated as unchanged). A value that is not an email address is rejected (422
invalid_body). Any change to the stored value is a reassignment: the ticket's token rotates and the assignment-notified stamp clears.
Response fields
legacy_refstring- The ref this ticket carried on the platform it was migrated from; empty for tickets sold on Tito. See the same field on Order.
answersarray of Answer- What this ticket's holder was asked, plus any metadata the organizer records against them. Always an array, never null — a ticket with neither serializes as
[]. Includes the metadata the organizer keeps to their own team: this read is gated by attendees.view, the very capability that gates the admin's own attendee page.A question can be asked because of the ticket type OR because of a session the ticket got this person into. An organizer can attach a question to a session offering and narrow it to some of its options, so 'what's your level of Rust?' is asked only of the people who picked the Rust workshop. Those answers arrive here, in this same array and this same shape — there is no separate field and nothing says which of the two asked it. Two holders of the same ticket type can therefore carry different questions, and moving somebody to another option drops the answers the new option does not ask for.
answers[].field_idintegeranswers[].keystring- The stable machine handle: unique, and fixed when the question or detail was created. Match on this rather than on
label, which the organizer may rewrite any afternoon. answers[].labelstring- The question, or the name of the detail, as the organizer wrote it, in the account's own language. Per-locale overlays are for people reading a page; a response has no reader whose language it could mean.
answers[].kindstring- How it was asked, and so how to read
value: "text", "textarea", "select", "multi_select", "checkbox", "yes_no", "number", "date", "phone" or "file". answers[].valuestring- The answer exactly as stored, never as displayed: a yes/no reads "yes" or "no" rather than the word a page would show it as, a ticked checkbox reads "yes", a multi-select is its choices as one comma-separated list, and a phone number is the digits as given. A question left blank is omitted from the list rather than reported as an empty answer. On a "file" question this names what was uploaded; the bytes themselves are not served by the API.
syncarray of SyncStanding- Where this ticket stands with each connected service. Always an array, never null — a ticket never sent to any service serializes as
[]. sync[].remote_idstring- The id the service knows this ticket by. Empty when nothing has synced yet.
sync[].synced_atstring (RFC 3339) or null- When the last successful sync completed; null when it has never succeeded.
sync[].errorstring- The service's own words for its last failure, verbatim. Empty when the last attempt succeeded.
Responses
200The updated ticket, in the same shape the ticket list returns. The ticket's token never appears here, rotated or not.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body is not a JSON object of the documented fields — malformed JSON, a wrong field type, an explicitnull(omit a field to leave it unchanged; a write never silently ignores something you did send), or an unknown field name (invalid_body) — or a supplied value is blank/not an email (invalid_body), or the ticket cannot carry a holder edit: it is void, or its type is anonymous (not_editable).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/tickets/H7K2PX \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"holder_name":"Ada Lovelace"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/tickets/H7K2PX", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"holder_name": "Ada Lovelace"
}),
});
const data = await res.json();
{
"ref": "T9QW4M",
"legacy_ref": "",
"status": "valid",
"ticket_type": "Early Bird",
"holder_name": "Ada Lovelace",
"holder_email": "ada@example.com",
"order_ref": "H7K2PX",
"created_at": "2026-05-18T14:03:07Z",
"answers": [
{
"field_id": 3,
"key": "dietary",
"label": "Dietary requirements",
"kind": "text",
"value": "Vegetarian"
}
],
"sync": [
{
"service": "example",
"remote_id": "example",
"state": "synced",
"synced_at": "2026-10-14T09:00:00Z",
"error": "example"
}
]
}
Refund a ticket
POST /tickets/{ref}/refund
Refund one ticket and void it. Refunds a single ticket's share of its own order line (line total ÷ quantity, so it can never over-refund) and voids the ticket, releasing its stock — the same ticket-level path the admin's single-line refund and the buyer's self-service removal use. Money moves: the key must carry the standalone refund capability. Send no request body; any body is ignored. The order itself stays paid; use the order refund to take back the whole thing. Idempotent: an already-void ticket answers 200 with already: true and never reaches Stripe. Unlike the admin's line refund, one ticket on a multi-ticket line is refundable here — a single ticket is exactly what this endpoint addresses. A ticket whose order is not paid, or that has no active line left to refund, answers 422 not_refundable; a ticket with nothing to refund against (payments unconfigured, or no recorded payment intent) answers 422 no_payment_intent. A zero-cent ticket needs neither and is simply voided.
Requires refund
Path parameters
refstring required- The ticket's ref (account-wide, not scoped to an event), matched against the ticket's own ref — an order ref is not a ticket ref, and answers 404 here. The ticket's token — the buyer's URL capability — is never accepted here and never returned.
Response fields
alreadyboolean- Present and true only when the refund had already happened before this call — the replay-safe case. No money moved and no provider call was made;
refunded_centsis omitted, since this call refunded nothing. refunded_centsinteger- Integer cents this call refunded (0 for a free/zero-cent ticket). Omitted when
alreadyis true.
Responses
200The ticket is refunded and void — either just now (withrefunded_cents, which is 0 for a free ticket) or already (already: true).401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The ticket is not refundable: its order is not paid, or it has no active order line left to refund (not_refundable); or there is no payment to refund it against (no_payment_intent).500Internal error.502The refund did not complete (refund_failed): either the payment provider refused it — in which case nothing moved and nothing changed locally — or it succeeded and the local write that follows it failed, which the instance journals as CRITICAL. The two are not distinguishable from here, and re-reading the resource does not separate them either (after the second case the order still readspaid). Retry: it is safe, and it is the repair. The provider idempotency key is derived from the order ref, so a retry replays the original refund rather than moving money twice, and the local write that failed gets another run. Left alone, the provider's own refund webhook converges the local state anyway.
curl -X POST https://<yourslug>.go.tito.io/api/v1/tickets/H7K2PX/refund \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/tickets/H7K2PX/refund", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"refunded": true,
"already": true,
"refunded_cents": 14900
}
Check-in
Check-in lists record who has arrived. Every list covers all of an event's tickets; state is per list. Door tools check in and undo here.
List check-in lists
GET /events/{slug}/checkin-lists
List an event's check-in lists.
Requires checkin.view
Path parameters
slugstring required- The event's slug.
Response fields
checkin_listsarray of CheckinListSummarycheckin_lists[].idinteger- The list's row id; check-in lists carry no ref (admin-only, never a public surface).
checkin_lists[].created_atstring (RFC 3339)- Integer Unix timestamp in storage, rendered as RFC 3339 UTC. Not nullable — a list always has a creation time (NOT NULL column).
checkin_lists[].checked_in_countinteger- Check-ins recorded against this list, counted exactly as the admin's door page counts them: only tickets that are still valid (voided tickets drop out), and on an entitlement-scoped list only tickets holding that entitlement. 0 when the list has no check-ins yet, never a missing/null value.
Responses
200All check-in lists for the event.checkin_listsis always[], never null, when there are none.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/checkin-lists \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/checkin-lists", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"checkin_lists": [
{
"id": 1,
"name": "Main entrance",
"created_at": "2026-09-01T08:00:00Z",
"checked_in_count": 412
}
]
}
Get a check-in list
GET /events/{slug}/checkin-lists/{id}
Get a check-in list's state (who has been checked in).
Requires checkin.view
Path parameters
slugstring required- The event's slug.
idinteger required- The check-in list's numeric row id (check-in lists carry no ref — admin-only, never a public surface).
Response fields
idintegernamestringcreated_atstring (RFC 3339)- Integer Unix timestamp in storage, rendered as RFC 3339 UTC. Not nullable — a list always has a creation time (NOT NULL column).
checked_in_countintegercheckinsarray of Checkin
Responses
200The list's metadata plus every check-in recorded against it, filtered exactly aschecked_in_countis (valid tickets only, entitlement-scoped where the list is).checkinsis always[], never null, when the list has none.idfrom another event's list 404s rather than leaking cross-event data.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/checkin-lists/1 \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/checkin-lists/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 1,
"name": "Ada Lovelace",
"created_at": "2026-10-14T09:00:00Z",
"checked_in_count": 42,
"checkins": [
{
"ticket_ref": "T9QW4M",
"checked_in_at": "2026-10-14T08:41:19Z"
}
]
}
Check a ticket in
POST /events/{slug}/checkin-lists/{id}/checkins
Check a ticket in on a list. Resolves ticket (a ticket ref) and stamps it onto the list via the same eligibility rule the admin door page uses (valid tickets only, entitlement-scoped where the list is). A fresh stamp and an already-stamped ticket both answer 200 — the latter is the idempotent, door-tool-friendly case (already: true). A ticket that fails eligibility (void, or ineligible for an entitlement-scoped list) answers 422 not_eligible. id must belong to the slug's event, and ticket must belong to the same event as the list — either mismatch, or an unknown list/ticket, answers 404.
Requires checkin.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The check-in list's numeric row id (check-in lists carry no ref — admin-only, never a public surface).
Request body
Response fields
alreadyboolean- Present and true only when the ticket was already stamped on this list before this call.
Responses
200The ticket is checked in on this list — either just now (alreadyabsent/false) or already (already: true), which is still a success, not an error.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body is not{"ticket":"<ref>"}(invalid_body), or the ticket is void or not eligible for this list's entitlement (not_eligible).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/checkin-lists/1/checkins \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"ticket":"T9QW4M"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/checkin-lists/1/checkins", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"ticket": "T9QW4M"
}),
});
const data = await res.json();
{
"checked_in": true,
"already": true
}
Undo a check-in
DELETE /events/{slug}/checkin-lists/{id}/checkins/{ref}
Undo a check-in (mis-scans happen at real doors). Idempotent: removing a stamp that doesn't exist (already undone, never stamped, or an unresolvable/cross-event ticket ref) is still 200, not an error. Only an unknown id/slug 404s.
Requires checkin.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The check-in list's numeric row id.
refstring required- The ticket's ref.
Response fields
Responses
200The ticket is not checked in on this list (either it just got undone, or there was nothing to undo).401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/events/annual-conf/checkin-lists/1/checkins/H7K2PX \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/checkin-lists/1/checkins/H7K2PX", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"checked_in": false
}
Reports
Income, availability and grouped order reports for one event, the same numbers the admin's dashboards show.
Availability report
GET /events/{slug}/availability
Report how much of every sellable thing is taken and how much is left. The data behind the admin's Availability report: every ticket type, session offering, good and donation on the event, with its cap, how much is sold, how much is held by pending orders, and what is left.
Pending counts as taken. An unpaid order holds its stock until it pays or its hold expires, so remaining is the cap less taken (sold plus pending) — for a good, literally the same ledger the checkout gate enforces, which is what makes this safe to drive a stock display from.
capacity and remaining are null when the thing has no limit, never 0 — a caller must not read an absent cap as sold out. Donations have no stock to run out of, so their capacity is always null.
Kinds and status words are English. Rows arrive grouped by kind in a fixed order (tickets, sessions, goods, donations); a kind the event doesn't sell contributes no rows.
Requires events.view
Path parameters
slugstring required- The event's slug.
Response fields
rowsarray of objectrows[].namestring- The organiser's own name for the thing. Never empty. A block of session options the organiser left unnamed is reported by when it runs ("Fri 5 Mar, 10:00–11:30", or both dates where the block spans more than one day), formatted in English like the rest of this response.
rows[].pendinginteger- Units under a live hold that has not been paid for — a checkout in progress, a portal basket, an extra added to an existing order. Stock that comes back if it is not paid in time.
rows[].via_portalsinteger- The share of
takenthat arrived through a portal rather than the main page — either bought in the portal's own store, or claimed from one of its invitations. Always present, including for events running no portals. Units, never money: this counts a portal's drawdown, not a revenue line. rows[].portalsarray of objectvia_portalssplit by the portal that took it, biggest share first. Always present;[]when no portal has taken any, and also[]for a key that does not carry theinvitations.viewscope — naming the portals is portal information, so a key that needs the split must carry that scope on top ofevents.view. Every figure,via_portalsincluded, is served either way. The entries sum tovia_portals: both are counted through this offering's own ledger. One caveat, and only forsessions— a held place carries no order until its ticket is issued, so a session's split can sum to less than itstaken.rows[].remaininginteger or null- capacity less taken, floored at 0; null when there is no limit. A 0 here means sold out.
rows[].statusstringsold out,off sale, or empty when the thing is simply on sale. Off sale wins over sold out.
Responses
200Every sellable thing, plus the totals the report opens with.rowsis always[], never null.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/availability \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/availability", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"rows": [
{
"kind": "tickets",
"name": "Ada Lovelace",
"capacity": 500,
"sold": 42,
"pending": 1,
"taken": 1,
"via_portals": 1,
"portals": [
{
"portal": "example",
"units": 1
}
],
"remaining": 1,
"status": "ticket"
}
],
"listed": 1,
"sold": 42,
"pending": 1,
"sold_out": 42
}
Income report
GET /events/{slug}/income
Report an event's income and deductions, with a month-by-month breakdown. The data behind the admin's Income and deductions report: gross before anything comes off, less discount codes, less refunds, giving what was collected from buyers; less the tax to remit, giving net of tax. Only orders that actually took money are counted, whichever stamp on names — money committed but not landed is reported separately as pending_cents and is in none of the other figures.
Then less what the payments cost: fees_cents is what the payment provider took and platform_fee_cents is Tito's own, giving net_cents — what the organizer keeps. Fees are never handed back on a refund, so a refunded order still carries the cost of taking it.
A payment whose cost is not established — a card charge Stripe has not settled yet, or one whose fee settled in a different currency from the order's — is counted in unpriced_payments and is in neither fee figure. It is not a payment that cost nothing, and a caller summing fees must treat a nonzero unpriced_payments as an incomplete total.
This is still not a profit and loss: nothing here knows what a venue, catering or staff cost, and none of it is what reaches a bank account. Tito does not remit the tax it reports here.
Months bucket in the event's own timezone (reported as timezone), and the month rows decompose the totals exactly.
Requires events.view
Path parameters
slugstring required- The event's slug.
Query parameters
rangestring- The window, measured against whichever stamp
onnames. One of30d,7d,24h,month(this calendar month to date),all, orcustom— which takes its bounds fromfromandtoand is the one range that does not travel with the clock. Anything else is30d. fromstring- The first day of a custom window, as
YYYY-MM-DD, cut at midnight in the event's own timezone. Read only whenrange=custom, and optional even then — a custom window with onlytoset runs from the first record. A date that will not parse is no bound at all, andrange=customwith neither bound falls back to30d. tostring- The last day of a custom window, as
YYYY-MM-DD, INCLUSIVE — the window runs to the end of that day in the event's own timezone. Read only whenrange=custom. Givenfromandtothe wrong way round, the window is still the one between them. onstring- Which stamp a paid order is dated by:
paid(default) orstarted. Pending money has no payment day either way, sopending_centsalways counts from the day the order started.
Response fields
fromstring- The first day of the window that ran, as
YYYY-MM-DD. Always present, empty unless the range iscustom. tostring- The last day of the window that ran, inclusive, as
YYYY-MM-DD. Always present, empty unless the range iscustom. currencystring- The event's currency. Orders taken in another currency are still summed into these amounts.
ordersinteger- Paid orders in the window — the ones every figure below except the pending pair is built from.
gross_centsinteger- Before anything comes off: what those orders charged plus the discount that came off them.
discount_centsinteger- Discount taken off by codes, as a positive amount. Recorded per ORDER, never per line, so it cannot be attributed to a ticket type.
collected_centsintegergross_centsminusdiscount_centsminusrefund_cents— what was actually collected from buyers.tax_centsinteger- Tax to remit, as a positive amount: the tax snapshotted on each order line when it was sold, so a rate change never rewrites a filed figure. Note the snapshot has no rate id, so two rates sharing a name and percentage are indistinguishable.
fees_centsinteger- What the payment provider took, as a positive amount. Counts only payments whose cost is established and settled in the event's own currency — see
unpriced_payments. platform_fee_centsinteger- Tito's own fee, as a positive amount. Always 0 for an account paying through its own Stripe key, which is charged no platform fee.
unpriced_paymentsinteger- How many payments in range have no established cost and are therefore in neither fee figure. Nonzero means the fee total is incomplete, not that those payments were free.
net_centsintegercollected_centsminustax_centsminusfees_centsminusplatform_fee_cents— what the organizer keeps of the money that moved, before anything the event itself cost.pending_centsinteger- Money committed but not landed: orders still pending, counted from the day they started. In none of the figures above.
monthsarray of IncomeMonthmonths[].labelstring- The same month worded for a human. Always English here, like every other machine-facing string in this API.
months[].gross_centsinteger- Before anything comes off: what the orders charged plus the discount that came off them.
months[].fees_centsinteger- What the payments cost, as a positive amount: the provider's cut and Tito's together. The ledger's own totals split the two.
Responses
200The ledger's own figures, the months behind them, and the query that actually ran.monthsanddiscount_codesare always[], never null; months sort oldest first.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/income \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/income", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"range": "example",
"from": "example",
"to": "example",
"on": "example",
"timezone": "Europe/Dublin",
"currency": "eur",
"orders": 1,
"gross_cents": 14900,
"discount_cents": 14900,
"discount_codes": [
"EARLYBIRD"
],
"refund_cents": 14900,
"refunded_orders": 1,
"collected_cents": 14900,
"tax_cents": 14900,
"fees_cents": 14900,
"platform_fee_cents": 14900,
"unpriced_payments": 1,
"net_cents": 14900,
"pending_cents": 14900,
"pending_orders": 1,
"months": [
{
"key": "2026-05",
"label": "May 2026",
"gross_cents": 909000,
"deductions_cents": 34900,
"tax_cents": 0,
"fees_cents": 18180,
"net_cents": 855920
}
]
}
Orders report
GET /events/{slug}/report
Report an event's orders grouped by day, week, month, audience, status or discount code. The data behind the admin's custom report: the same grouping, range and filters, aggregated at ORDER grain over orders.amount_cents — what actually changed hands, discounts and non-ticket items included. Rows carry no personal data, and there is no drill-through to the orders behind a row. Every parameter clamps to a known value rather than erroring, so the response echoes the query it actually ran. Days bucket in the event's own timezone (reported as timezone). This endpoint reports on ORDERS only, and source is the one parameter that does not clamp: it must be omitted or orders, and anything else is a 400. A grouping is a different view of the same records, so falling back to a default is safe; a source is a different record set, so answering an attendee question with order figures under a 200 would be a wrong number no caller could see was wrong. The admin's other sources carry personal data under their own capabilities, and widening this key scope to reach them is a decision that has not been taken.
Requires events.view
Path parameters
slugstring required- The event's slug.
Query parameters
sourcestring- The record set to report on. Only
ordersis served here; omit it or passorders. Any other value is rejected withunsupported_sourcerather than quietly answered with order figures. groupstring- The axis rows are grouped on. One of
day,week(weeks start Monday),month,audience,status,code(discount codes). Anything else isday. rangestring- The window, measured against whichever stamp
onnames. One of30d,7d,24h,month(this calendar month to date),all, orcustom— which takes its bounds fromfromandtoand is the one range that does not travel with the clock. Anything else is30d. fromstring- The first day of a custom window, as
YYYY-MM-DD, cut at midnight in the event's own timezone. Read only whenrange=custom, and optional even then — a custom window with onlytoset runs from the first record. A date that will not parse is no bound at all, andrange=customwith neither bound falls back to30d. tostring- The last day of a custom window, as
YYYY-MM-DD, INCLUSIVE — the window runs to the end of that day in the event's own timezone. Read only whenrange=custom. Givenfromandtothe wrong way round, the window is still the one between them. onstring- Which stamp an order is dated by:
paid(default) orstarted. Measured against payment, an order that has never been paid has no date and is simply not in the report — filter bystatus=pendingto see those. statusstring- Keep only orders in this status. One of
paid,pending,refunded,canceled; anything else (including absent) means any status. audiencestring- Keep only orders from this audience, matched against the same bucket
group=audienceputs the order in — an audience's slug, or-for orders that came in off the event's own page. Absent means any audience. codestring- Keep only orders carrying this discount code, matched against the same bucket
group=codeputs the order in — the codes an order redeemed, comma-separated in redemption order when it redeemed more than one, or-for orders with no code at all. Absent means any. currencystring- Read the report in this currency — one at a time, because money is never summed across currencies. Only a currency the event sells in is accepted; absent, or anything else, reads in the event's own
currency. Call once per entry of the event'scurrenciesto read them all. Echoed back ascurrency. paidstring1keeps only orders that actually took money, whichever stamponnames — which is not the same asstatus=paid, because a refunded order did take money. Anything else (including absent) keeps every order. Measured against payment this changes nothing, since an unpaid order has no paid day to be counted on.
Response fields
fromstring- The first day of the window that ran, as
YYYY-MM-DD. Always present, empty unless the range iscustom. tostring- The last day of the window that ran, inclusive, as
YYYY-MM-DD. Always present, empty unless the range iscustom. audiencestring- The audience filter that ran; empty means any audience,
-means orders that came in off the event's own page. rowsarray of ReportRowrows[].keystring- The bucket's machine-stable id: an ISO date (
dayis2026-08-05,weekthe Monday it starts,month2026-08), an audience's flow slug, a status, a discount code, or a lowercase currency code. Legitimately EMPTY for the main page's audience, an order with no discount code, and the totals line. rows[].labelstring- The same bucket worded for a human. Always English here, like every other machine-facing string in this API — the admin page localizes it; a caller with its own wording should read
key. rows[].gross_centsinteger- What changed hands, before refunds — the orders' own amounts, so discounts are already applied and non-ticket items are included.
rows[].tax_centsinteger- Tax to remit on those orders, as a positive amount: the tax snapshotted on each order line when it was sold, summed over the lines still standing. An order refunded in full contributes nothing, and a ticket refunded off an order takes its own line's tax with it. NOT subtracted from
net_cents— see/events/{slug}/incomefor the ledger that walks it down. rows[].currencystring- The event's currency. Orders taken in another currency are still summed into these amounts — group by
currencyto split them apart. totalsobject- One grouped row of the custom report — and, in
totals, the same shape summing every row. Amounts are integer minor units ofcurrency(never a float), aggregated at order grain. totals.keystring- The bucket's machine-stable id: an ISO date (
dayis2026-08-05,weekthe Monday it starts,month2026-08), an audience's flow slug, a status, a discount code, or a lowercase currency code. Legitimately EMPTY for the main page's audience, an order with no discount code, and the totals line. totals.labelstring- The same bucket worded for a human. Always English here, like every other machine-facing string in this API — the admin page localizes it; a caller with its own wording should read
key. totals.gross_centsinteger- What changed hands, before refunds — the orders' own amounts, so discounts are already applied and non-ticket items are included.
totals.tax_centsinteger- Tax to remit on those orders, as a positive amount: the tax snapshotted on each order line when it was sold, summed over the lines still standing. An order refunded in full contributes nothing, and a ticket refunded off an order takes its own line's tax with it. NOT subtracted from
net_cents— see/events/{slug}/incomefor the ledger that walks it down. totals.currencystring- The event's currency. Orders taken in another currency are still summed into these amounts — group by
currencyto split them apart.
Responses
200The grouped rows and the totals line, plus the query that actually ran.rowsis always[], never null, when nothing falls in the window. Time groupings sort oldest first; every other axis sorts by gross, biggest first.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/report \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/report", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"source": "example",
"group": "example",
"range": "example",
"from": "example",
"to": "example",
"on": "example",
"status": "ticket",
"audience": "example",
"code": "EARLYBIRD",
"paid": true,
"timezone": "Europe/Dublin",
"currency": "eur",
"rows": [
{
"key": "2026-W20",
"label": "Week of 11 May",
"orders": 38,
"tickets": 61,
"gross_cents": 909000,
"discount_cents": 20000,
"refund_cents": 14900,
"tax_cents": 0,
"net_cents": 874100,
"currency": "eur"
}
],
"totals": {
"key": "2026-W20",
"label": "Week of 11 May",
"orders": 38,
"tickets": 61,
"gross_cents": 909000,
"discount_cents": 20000,
"refund_cents": 14900,
"tax_cents": 0,
"net_cents": 874100,
"currency": "eur"
}
}
List saved reports
GET /events/{slug}/reports
List an event's saved reports. The reports somebody at this account named and kept. Built-in reports are absent by design — they are the same handful of definitions on every event, not rows here. Each report's whole definition is its query: append it to the report endpoint (or the admin's own report URL) to run it.
Requires events.view
Path parameters
slugstring required- The event's slug.
Response fields
reportsarray of SavedReportreports[].idinteger- The report's row id; saved reports carry no ref (admin-only, never a public surface).
reports[].querystring- The whole definition, as a URL query string — grouping, date range, what it measures against, and any filters. Empty means the report's defaults (grouped by day, last 30 days, measured against the day an order was paid).
reports[].created_atstring (RFC 3339)- Integer Unix timestamp in storage, rendered as RFC 3339 UTC. Not nullable (NOT NULL column).
reports[].updated_atstring (RFC 3339)- When the name or the definition last changed. Equals
created_aton a report nobody has edited.
Responses
200The event's saved reports, oldest first.reportsis always[], never null, when there are none.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/reports \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/reports", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"reports": [
{
"id": 1,
"name": "Sales by week",
"source": "orders",
"query": "group=week\u0026range=all",
"created_at": "2026-05-01T09:00:00Z",
"updated_at": "2026-05-01T09:00:00Z"
}
]
}
Widgets
The embeddable ticket lists an organizer pastes into their own site, and the domains they have been seen on.
List widgets
GET /events/{slug}/widgets
List an event's widgets (the ticket lists an organiser pastes into their own website).
Requires events.view
Path parameters
slugstring required- The event's slug.
Response fields
widgetsarray of WidgetSummarywidgets[].idintegerwidgets[].slugstring- What the pasted markup names. Unique across the ACCOUNT, not per event, so the snippet can carry the widget's name and nothing else.
widgets[].shapestring- Lives on the record, never in the pasted markup — changing it does not change the organiser's website.
- one of
listbutton widgets[].secured_lineboolean- Whether the widget shows the “Secured by Tito” line. On by default, switchable.
widgets[].ticket_type_idsarray of integer- The ticket types this widget lists, in its own order. Empty means it lists everything on sale.
widgets[].installedboolean- Whether the widget has ever rendered on an approved website. Once true it never returns to false.
widgets[].installed_atstring (RFC 3339) or nullwidgets[].closedboolean- Whether the organiser has switched this widget off. A closed widget still answers on their website and says tickets are not on sale, because an empty box there reads as a broken widget.
widgets[].closed_atstring (RFC 3339) or nullwidgets[].created_atstring (RFC 3339)
Responses
200Every widget on the event.widgetsis always[], never null. The pasteable snippet is deliberately not returned: it is built from the widget's slug and the account's own host, and pinning that string here would freeze a format that belongs to the widget.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/widgets \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events/annual-conf/widgets", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"widgets": [
{
"id": 1,
"slug": "main",
"name": "Homepage list",
"shape": "list",
"secured_line": false,
"ticket_type_ids": [
1
],
"installed": true,
"installed_at": "2026-04-10T10:00:00Z",
"closed": false,
"closed_at": null,
"created_at": "2026-04-01T09:00:00Z"
}
]
}
List widget domains
GET /widget-domains
List the websites the account's widgets have been seen on, and their standing.
Requires events.view
Response fields
widget_domainsarray of WidgetDomainwidget_domains[].idintegerwidget_domains[].domainstring- Normalised host: lowercase, no scheme, no port, no leading www. Approving the bare form covers the www form too, so one row is one website.
widget_domains[].statusstring- pending = seen, nobody has decided yet. Only approved renders.
- one of
pendingapprovedblocked widget_domains[].sightingsintegerwidget_domains[].first_seen_atstring (RFC 3339)widget_domains[].last_seen_atstring (RFC 3339)
Responses
200Account-level, not event-scoped: a domain is a statement of trust about a website, not a property of one ticket list. Rows only ever arrive by being SEEN — an organiser never types a domain. Onlyapproveddomains are named in the widget'sframe-ancestorsresponse header, which is what the browser enforces.widget_domainsis always[], never null.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/widget-domains \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/widget-domains", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"widget_domains": [
{
"id": 1,
"domain": "annualconf.example",
"status": "approved",
"sightings": 318,
"first_seen_at": "2026-04-10T10:00:00Z",
"last_seen_at": "2026-08-27T18:12:00Z"
}
]
}
Webhooks
Outbound webhook endpoints and their delivery log. Endpoints are managed in the admin; this is the read side.
List webhook endpoints
GET /webhooks
List the account's webhook endpoints. The account's outbound-webhook subscriptions (Settings → Webhooks). webhooks is always [], never null, when there are none. The signing secret is never returned here or anywhere else in /api/v1 — it is admin-shaped data, shown only on the Settings → Webhooks detail page. Endpoint create/delete are not in v1's API surface for the same reason (see below); this route and the deliveries route beneath it are read-only.
The outbound contract, in full — everything a receiver needs to verify and parse a delivery:
Envelope. Every delivery POSTs this JSON body: {"id": <delivery id, integer>, "type": "<event type>", "created_at": <integer Unix timestamp>, "account": "<account slug>", "data": { ... event-specific payload ... }}.
Event types (the only values event_types may name, and the only values type ever carries): order.created, order.paid, order.cancelled (two l's — the workflow engine's own trigger spelling, which this vocabulary is drawn from verbatim, superseding an earlier single-l draft), order.refunded, ticket.created, ticket.voided, ticket.checked_in, ticket.updated, checkin.created. A checkin.created delivery fires once per fresh check-in stamp, on every surface that records one (the door scanner, the desk, group check-in, and the API), and its data adds a checkin object beside the usual ticket/order/event/account blocks: {"id": <check-in id>, "checked_in_at": <integer Unix timestamp>, "checked_in_at_text": "<localized>", "checkin_list": {"id": ..., "name": "..."}} — the same moment ticket.checked_in fires, but naming which check-in list the ticket came through. Undoing a check-in sends nothing; a later re-check-in delivers a fresh checkin.created.
Request headers on every delivery attempt: Tito-Signature: t=<unix timestamp>,v1=<hex-encoded HMAC-SHA256> — the same signed-timestamp-plus-body shape Stripe's own webhook signatures use; Tito-Webhook-Event: <event type>, the same string as the envelope's type; Tito-Webhook-Delivery: <delivery id>, the same integer as the envelope's id, useful for receiver-side deduplication since delivery is at-least-once.
Verifying a delivery: read t and v1 out of Tito-Signature; recompute HMAC-SHA256, keyed with the endpoint's own signing secret (shown once per visit on its Settings → Webhooks detail page, stored plaintext there because it must be recoverable to sign outgoing requests), over the byte string "<t>." + <raw request body>; compare the resulting hex digest to v1 with a constant-time comparison. Reject the delivery if they don't match, or if t is further in the past than your own replay-window tolerance.
Retry schedule: a failed attempt (transport error, or a non-2xx status) is retried at 0s (the first attempt itself), then after 1 minute, 5 minutes, 30 minutes, 2 hours, and 8 hours — 6 attempts total before a delivery is marked permanently failed. A 2xx response at any attempt marks it succeeded and stops the schedule.
Auto-disable: an endpoint that racks up 10 consecutive permanently-failed deliveries (each one having exhausted its own 6-attempt schedule) is automatically disabled — no further deliveries are attempted — and the organizer is emailed a notice. A manual re-enable from Settings → Webhooks clears the failure count and resumes delivery; it does not replay what was missed (v1 has no manual redeliver — the retry schedule plus this visible delivery log are the exposed mechanism instead of a second one).
Requires settings.manage
Response fields
webhooksarray of WebhookEndpointwebhooks[].idintegerwebhooks[].event_typesarray of string- The event types this endpoint is subscribed to — a non-empty subset of the fixed v1 vocabulary.
webhooks[].enabledboolean- False either because an admin turned it off, or because the drainer auto-disabled it after 10 consecutive permanently-failed deliveries (distinguish the two by
disabled_at: auto-disable always stamps it, a manual toggle never does). webhooks[].consecutive_failuresinteger- Consecutive permanently-failed deliveries (each having exhausted its own retry schedule) since the last success. Resets to 0 on the next successful delivery, or when the endpoint is re-enabled.
webhooks[].created_atstring (RFC 3339)webhooks[].disabled_atstring (RFC 3339) or null- Null unless the drainer auto-disabled this endpoint (10 consecutive permanently-failed deliveries) — a manual disable never sets this, which is what makes its presence the auto-disabled signal.
Responses
200The account's webhook endpoints, newest first.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/webhooks \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/webhooks", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"webhooks": [
{
"id": 1,
"url": "https://example.com/hooks/tito",
"event_types": [
"order.created"
],
"enabled": true,
"consecutive_failures": 0,
"created_at": "2026-04-01T09:00:00Z",
"disabled_at": null
}
]
}
List deliveries
GET /webhooks/{id}/deliveries
List one endpoint's recent delivery attempts. The endpoint's delivery log, newest first, capped at 50 rows — the same query and the same limit the Settings → Webhooks detail page reads. deliveries is always [], never null, when the endpoint has none yet. Terminal deliveries (succeeded or failed) older than 90 days are pruned from this log; pending deliveries are never pruned.
Requires settings.manage
Path parameters
idinteger required- The webhook endpoint's numeric row id.
Response fields
deliveriesarray of WebhookDeliverydeliveries[].event_typestring- Also the value sent as the Tito-Webhook-Event header on the wire. "webhook.test" is a test delivery sent by hand from the admin rather than a real account event, so it is not a type an endpoint can subscribe to.
deliveries[].statusstring- "pending" is still retrying (or awaiting its first attempt); "succeeded" got a 2xx response; "failed" exhausted the 6-attempt retry schedule without one.
- one of
pendingsucceededfailed deliveries[].next_attempt_atstring (RFC 3339)- When the next attempt is due, for a delivery still on the retry schedule (6 attempts spread over about ten and a half hours). In the past for one that is due now; carries no meaning once status leaves "pending".
deliveries[].last_status_codeinteger- The HTTP status the receiver answered on the most recent attempt. 0 when no attempt has completed yet, or every attempt so far failed at the transport level (refused/unreachable target, timeout) rather than getting an HTTP response.
deliveries[].last_errorstring- The most recent attempt's failure reason (transport error, or "endpoint answered <code>" for a non-2xx response), truncated to 500 characters. Empty once a delivery has succeeded.
deliveries[].created_atstring (RFC 3339)deliveries[].delivered_atstring (RFC 3339) or null- Null until this delivery succeeds; set once, on the attempt that got a 2xx.
Responses
200Up to the 50 most recent delivery attempts for this endpoint.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/webhooks/1/deliveries \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/webhooks/1/deliveries", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"deliveries": [
{
"id": 1,
"event_type": "order.paid",
"status": "succeeded",
"attempts": 1,
"next_attempt_at": "",
"last_status_code": 200,
"last_error": "",
"created_at": "2026-05-18T14:03:08Z",
"delivered_at": "2026-05-18T14:03:09Z"
}
]
}
Workflows
Automations that run when something happens in the account, and the runs they produced.
List workflows
GET /workflows
Requires settings.manage
Response fields
workflowsarray of Workflowworkflows[].idintegerworkflows[].namestringworkflows[].enabledbooleanworkflows[].all_eventsboolean- Whether this workflow fires for every event, including ones created later (the workflow_subscriptions event_id=0 sentinel). When true, events is always [].
workflows[].created_atstring (RFC 3339) or nullworkflows[].updated_atstring (RFC 3339) or nullworkflows[].eventsarray of string- Slugs of the events subscribed to this workflow. Always an array, never null, when the workflow has none.
workflows[].definitionobject- The workflow's node/edge graph (internal/workflow.Definition), verbatim as stored — organizer-authored, no secrets. Present only on the single-workflow endpoint, and only when the stored definition parses as JSON; see definition_raw.
workflows[].definition_rawstring- The raw, unparsed definition text. Present instead of definition, on the single-workflow endpoint only, when the stored JSON fails to parse — a corrupt definition must not break this endpoint.
Responses
200Every workflow, without its definition.workflowsis always[], never null, when the account has none.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/workflows \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/workflows", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"workflows": [
{
"id": 1,
"name": "Thank-you email",
"enabled": true,
"all_events": true,
"created_at": "2026-04-01T09:00:00Z",
"updated_at": "2026-04-01T09:00:00Z",
"events": [
"example"
],
"definition": {},
"definition_raw": "example"
}
]
}
Get a workflow
GET /workflows/{id}
Get a workflow by id, including its definition.
Requires settings.manage
Path parameters
idinteger required- The workflow's numeric row id.
Response fields
idintegernamestringenabledbooleanall_eventsboolean- Whether this workflow fires for every event, including ones created later (the workflow_subscriptions event_id=0 sentinel). When true, events is always [].
created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or nulleventsarray of string- Slugs of the events subscribed to this workflow. Always an array, never null, when the workflow has none.
definitionobject- The workflow's node/edge graph (internal/workflow.Definition), verbatim as stored — organizer-authored, no secrets. Present only on the single-workflow endpoint, and only when the stored definition parses as JSON; see definition_raw.
definition_rawstring- The raw, unparsed definition text. Present instead of definition, on the single-workflow endpoint only, when the stored JSON fails to parse — a corrupt definition must not break this endpoint.
Responses
200The workflow plus its subscribed events and its definition —definitionwhen the stored JSON parses,definition_raw(the raw text) when it does not, so a corrupt definition never breaks this endpoint.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/workflows/1 \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/workflows/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 1,
"name": "Thank-you email",
"enabled": true,
"all_events": true,
"created_at": "2026-04-01T09:00:00Z",
"updated_at": "2026-04-01T09:00:00Z",
"events": [
"example"
],
"definition": {},
"definition_raw": "example"
}
List runs
GET /workflows/{id}/runs
List a workflow's runs (paged, newest first).
Requires settings.manage
Path parameters
idinteger required- The workflow's numeric row id.
Query parameters
afterinteger- Cursor: the run id returned as the previous page's
next_after. Omit for the first page. limitinteger- Page size. Default 50, max 200. Non-positive or unparseable values fall back to the default.
Response fields
runsarray of WorkflowRunruns[].idintegerruns[].statusstring- "pending", "running", "completed", or "failed". A run that fails on something worth retrying — a 5xx, a 429, or no answer at all — goes back to "pending" for its next attempt rather than straight to "failed".
- one of
pendingrunningcompletedfailed runs[].attemptinteger- How many attempts have been made. A "pending" run with attempt above 0 is waiting on a retry rather than queued for the first time. Retries back off a minute, five, half an hour, then two hours; after that the run fails for good.
runs[].created_atstring (RFC 3339) or nullruns[].logarray of any- The run's step log (internal/workflow.Step array), verbatim as stored. Present only on the single-run endpoint. Falls back to a JSON string of the raw stored text when it fails to parse as JSON — the log is a debugging surface and must not break this endpoint on a malformed row.
next_afterinteger or null- Run id to pass as
?after=for the next page; null when there are no further pages.
Responses
200A page of runs, newest first.runsis always[], never null, when the workflow has none.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/workflows/1/runs \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/workflows/1/runs", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"runs": [
{
"id": 1,
"trigger": "order.paid",
"subject_type": "order",
"subject_id": 1,
"status": "completed",
"attempt": 1,
"created_at": "2026-05-18T14:03:08Z",
"started_at": "2026-05-18T14:03:08Z",
"finished_at": "2026-05-18T14:03:09Z",
"log": [
null
]
}
],
"next_after": 1
}
Get a run
GET /workflows/{id}/runs/{runID}
Get one run, including its step log.
Requires settings.manage
Path parameters
idinteger required- The workflow's numeric row id.
runIDinteger required- The run's numeric row id (run ids are a single global sequence across every workflow in the account — a run belonging to a different workflow than the one in the path 404s).
Response fields
idintegerstatusstring- "pending", "running", "completed", or "failed". A run that fails on something worth retrying — a 5xx, a 429, or no answer at all — goes back to "pending" for its next attempt rather than straight to "failed".
- one of
pendingrunningcompletedfailed attemptinteger- How many attempts have been made. A "pending" run with attempt above 0 is waiting on a retry rather than queued for the first time. Retries back off a minute, five, half an hour, then two hours; after that the run fails for good.
created_atstring (RFC 3339) or nulllogarray of any- The run's step log (internal/workflow.Step array), verbatim as stored. Present only on the single-run endpoint. Falls back to a JSON string of the raw stored text when it fails to parse as JSON — the log is a debugging surface and must not break this endpoint on a malformed row.
Responses
200The run plus its step log (log, present only here).401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/workflows/1/runs/1 \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/workflows/1/runs/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 1,
"trigger": "order.paid",
"subject_type": "order",
"subject_id": 1,
"status": "completed",
"attempt": 1,
"created_at": "2026-05-18T14:03:08Z",
"started_at": "2026-05-18T14:03:08Z",
"finished_at": "2026-05-18T14:03:09Z",
"log": [
null
]
}
Export
The whole account as one SQLite file: the escape hatch for BI, ad-hoc SQL, and portability.
Export the database
GET /export/db
A VACUUM INTO snapshot of the account's SQLite file, streamed and then discarded server-side. A full DB download is a full PII export, so it gates behind export.pii rather than any of the narrower .view capabilities. Admin-origin secrets are scrubbed from the snapshot before it streams: the csrf_token and admin_token_hash rows of config are deleted. Buyer capability tokens (orders.token, tickets.token) are account data and do ride along.
Requires export.pii
Responses
200The account's SQLite database file.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/export/db \
-H "Authorization: Bearer titogo_sk_your_key"
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/export/db", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
Meta
The contract itself.
OpenAPI document
GET /openapi.json
Public: it's the contract, not data. No Authorization header required.
No key required
Responses
200This OpenAPI 3.1 document.
curl https://<yourslug>.go.tito.io/api/v1/openapi.json
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/openapi.json", {
method: "GET",
headers: {
},
});
const data = await res.json();
{}
Objects
The shapes the endpoints above return, one attribute at a time.
The Error object
The one error shape returned by every route.
Attributes
{
"error": {
"code": "not_found",
"message": "no such event"
}
}
The RefundResult object
What a refund endpoint answers. refunded is always true on a 200 — a refusal is an error response, never a false here.
Attributes
alreadyboolean- Present and true only when the refund had already happened before this call — the replay-safe case. No money moved and no provider call was made;
refunded_centsis omitted, since this call refunded nothing. refunded_centsinteger- Integer cents this call refunded (0 for a free/zero-cent ticket). Omitted when
alreadyis true.
{
"refunded": true,
"already": true,
"refunded_cents": 14900
}
The CancelResult object
What the cancel endpoint answers on a 200 — always a fresh cancellation; a non-pending order is an error response, never a false here.
Attributes
{
"canceled": true,
"ref": "H7K2PX"
}
The MarkPaidResult object
What the mark-paid endpoint answers on a 200 — always a fresh fulfillment; an order that was already paid is an error response, never a false here.
Attributes
{
"paid": true,
"ref": "H7K2PX"
}
The OrderBuyerPatch object
The order's own buyer details. Send either field, both, or neither (an empty object changes nothing). An absent field is left as it was; null is refused.
Attributes
revoke_linkboolean- Take the order away from the address it is moving off: the order's own link is replaced, and so is the link of every ticket that address was sent (a ticket belonging to somebody else's address keeps its own — naming a holder already rotated it). Ignored unless
emailactually changes the address. Nothing is emailed, so the buyer is left without a working link until you send them one.
{
"name": "Ada Lovelace",
"email": "ada@example.com",
"revoke_link": true
}
The Account object
Attributes
regionstring- Where the account's data currently lives, on deployments that place accounts by region. Absent on a standalone instance with no placement — never an empty string.
{
"slug": "acme",
"name": "Acme Events",
"region": "uk"
}
The Event object
An event: something you sell tickets to. Addressed everywhere by its slug.
Attributes
pathstring- The event's public address. Equal to the slug for an event in no series; for one inside a series the series carries the first segment, so the address reads "meet-tito/london" while the slug stays "meet-tito-london". Every endpoint here is keyed on the slug — link buyers to the path.
venue_addressstring- Organizer-entered address text, in the event's default locale. Empty when no structured venue is set.
venue_latstring- Decimal latitude of the venue's map pin, as a string. Empty when the event has no pin.
venue_lngstring- Decimal longitude of the venue's map pin, as a string. Empty when the event has no pin.
venue_place_idstring- Google's opaque place id for the venue, if the address was chosen from the picker. Empty otherwise.
map_providerstring- Which service the organizer asked to draw the map on the event page. A stored preference, not a promise about what people see: an event set to "apple" on an instance without Apple credentials falls back to the Google embed, and an event with no map credentials at all shows the address and its open-in links.
- one of
googleapple currenciesarray of string- Every currency the event sells in:
currencyfirst, then the extra currencies the organizer priced. A one-currency event lists justcurrency. currency_assignmentstring- How a buyer lands in one of
currencies: a picker on the event page, the browser's country, or only a link carrying?currency=. Meaningful only whencurrencieshas more than one entry. - one of
switcherlocalelink created_atstring (RFC 3339) or null- Null when the underlying Unix timestamp is 0 (should not occur in practice for created_at, but the same nullable encoding is used for every timestamp field).
draftboolean- True while the event is a draft: its public page answers 404 and it is absent from the account's public event list. A duplicated event lands as a draft until it is published.
secretboolean- True when the event is secret: finished and selling, but deliberately not advertised. Unlike no_index this DOES gate visibility and a listing must respect it — the event's main page answers 404 to everybody but a signed-in organizer, and the event is absent from the account's public event list and from its series page. It is not a second draft: a draft can take no money, while a secret event sells all day through every door its organizer handed somebody on purpose (audience pages, portal links, invitations, and order and ticket links already sent). Read it as "do not advertise", never as "closed for business".
no_indexboolean- True when the organizer has asked search engines not to list this event. Unlike draft this gates NOTHING: the event's page answers exactly as it did before, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells. It is independent of secret in both directions, so this flag says nothing about whether the event is reachable — read secret for that. All it does is add a robots noindex directive to the event's own page, at every address that page answers on. Do not treat it as a visibility flag; the one thing it should change for an integration is that a page rebuilt elsewhere carries the same request across.
seriesobject- The series this event belongs to, absent when it is in none. An event belongs to at most one.
series.slugstringseries.namestringfieldsobject- The event's DETAILS — content the organizer typed about this event (a video-call link, a hashtag) under Settings, Metadata, keyed by the detail's short name, and what a theme reads as event.fields.<key>. Only details about the EVENT appear; anything about a person is a per-attendee answer or a per-attendee detail (some of them internal) and is never included. The value is resolved: a detail this event has not answered for itself reports its account default, because that is what every page and email will show. A detail with no value anywhere is omitted rather than reported blank — blank is a normal answer for a detail. Every value is a string, a number detail included: it reports its digits with no grouping (
50000), so it parses, while the organizer's own screens and a themed page write it out for their reader's language. Present on the single-event endpoint only, and omitted entirely when the event has nothing set. sectionsarray of EventSection- The event page's sections, in the order people meet them — what a ticket type's section_id points at. Present on the single-event endpoint only.
sections[].idintegersections[].namestring- The heading people read above this section. Empty when the section deliberately has none — its things render with nothing above them. A section still carrying one of the platform's own default headings reports it in English; this API negotiates no locale.
sections[].positioninteger- Zero-based place in the page, top to bottom. The list is already returned in this order.
{
"slug": "annual-conf",
"path": "annual-conf",
"name": "Annual Conference 2026",
"description": "Two days of talks, workshops and hallway conversations.",
"venue": "The Round Room, Dublin",
"venue_address": "Rotunda, Parnell Square, Dublin 1",
"venue_lat": "53.3529",
"venue_lng": "-6.2634",
"venue_place_id": "",
"map_provider": "google",
"starts_at": "2026-10-14T09:00:00Z",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"draft": false,
"secret": false,
"no_index": false,
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
The EventSection object
One section of the event page: a heading and a place, and nothing else. There are no dates and no visibility rules — a section that can be scheduled is a different feature.
Attributes
idintegernamestring- The heading people read above this section. Empty when the section deliberately has none — its things render with nothing above them. A section still carrying one of the platform's own default headings reports it in English; this API negotiates no locale.
positioninteger- Zero-based place in the page, top to bottom. The list is already returned in this order.
{
"id": 1,
"name": "Tickets",
"position": 1
}
The TicketTier object
Attributes
idintegernamestring- The rung's public name, and often empty — a blank name is a real answer, and people then see the price on its own.
quantityinteger or null- This rung's own allocation, on the same convention as the type's: null when uncapped, otherwise how many exist, and 0 means none available. It is NOT a share of the type's total — the type's total is the sum of the rungs. What the rung can actually sell may be larger than this, because a rung whose window closed with tickets unsold rolls them forward to the next one.
soldinteger- Seats bought at this rung, from paid orders. Every order line records the rung it was bought at, so this stays attributed to the price actually paid even after the ladder has moved on — which is also what returns a refunded ticket to the right rung.
ends_atstring (RFC 3339) or null- When the rung closes, EXCLUSIVE — the same contract as a product's sales_end_at. null for no end. RFC 3339.
liveboolean- Whether this is the rung on sale right now. At most one rung is live, and none is when the ladder sits between windows or every rung is spent.
pricesobject- This rung's price in every currency it is offered in, minor units keyed by lowercase currency code — the type's own
currency→price_centsalways, plus each extra currency the organizer priced the rung in. A currency the event sells in but this rung has no key for is one the type is not offered in while this rung is live.
{
"id": 1,
"name": "First 100",
"price_cents": 12900,
"quantity": 100,
"sold": 100,
"starts_at": null,
"ends_at": "2026-06-01T00:00:00Z",
"live": false,
"prices": {
"eur": 14900,
"gbp": 12900
}
}
The TicketBand object
One rung of a volume ladder. It has no id, no name and no live flag, and that is the difference from a price tier: a band is not a thing that opens and closes on a clock, it is a price the size of the order selects. Which band applies is therefore a property of the basket, not of the moment.
Attributes
from_qtyinteger- The order quantity this band starts at — two or more, always. One ticket is the type's own price_cents.
price_centsinteger- What ONE ticket costs from from_qty upwards. Integer cents; see the type's currency.
pricesobject- This band's price in every currency it is offered in, minor units keyed by lowercase currency code — the type's own
currency→price_centsalways, plus each extra currency the organizer priced the band in. A currency the type is sold in but this band has no key for is one the band is not offered in: an order in that currency pays the band above it.
{
"from_qty": 3,
"price_cents": 8000,
"prices": {
"eur": 8000,
"gbp": 6900
}
}
The TicketType object
Attributes
idintegernamestringprice_centsinteger- Integer cents; see currency. On a ticket with price tiers this is the LIVE rung's price — what a buyer would be charged right now — so a caller that only wants the price needs to know nothing about tiers. Between two tier windows with no bridge price set it falls back to the type's own dormant base and nothing can be bought;
tiersis what tells the two apart. currencystring- Lowercase ISO 4217 currency code — the event's currency; ticket types carry no currency of their own.
pricesobject- The type's price in every currency it is offered in, minor units keyed by lowercase currency code —
currency→price_centsalways, plus each extra currency the organizer priced it in. A currency the event sells in but this type has no key for is one the type is not offered in. on_salebooleanquantityinteger or null- Capacity for this type: null when the type is uncapped, otherwise how many exist. 0 is a real answer and means none available — the type reads as sold out. Until 2026-08-12 this was always an integer and 0 meant unlimited, which left no way to say sold out; a caller that special-cased 0 must now special-case null.
soldinteger- Count of this type's tickets that are still status='valid' — the same rule the admin's sold count uses. A voided ticket drops out.
tiersarray of TicketTier- The ticket's price ladder, in the order people move through it. Absent entirely for a ticket priced once, which is most of them. A tiered ticket is still ONE ticket type — one row on the event page, one set of questions, one cap — that charges a different amount as tickets sell or as dates pass. This is the LIVE ladder: a tier the organizer removed after it had sold tickets is hidden from it, because nobody can buy at that tier again — but its sales are still counted in the type's own
sold, so the tiers here will not always add up to it. The organizer's Sales report is where a removed tier is still named. tiers[].idintegertiers[].namestring- The rung's public name, and often empty — a blank name is a real answer, and people then see the price on its own.
tiers[].quantityinteger or null- This rung's own allocation, on the same convention as the type's: null when uncapped, otherwise how many exist, and 0 means none available. It is NOT a share of the type's total — the type's total is the sum of the rungs. What the rung can actually sell may be larger than this, because a rung whose window closed with tickets unsold rolls them forward to the next one.
tiers[].soldinteger- Seats bought at this rung, from paid orders. Every order line records the rung it was bought at, so this stays attributed to the price actually paid even after the ladder has moved on — which is also what returns a refunded ticket to the right rung.
tiers[].ends_atstring (RFC 3339) or null- When the rung closes, EXCLUSIVE — the same contract as a product's sales_end_at. null for no end. RFC 3339.
tiers[].liveboolean- Whether this is the rung on sale right now. At most one rung is live, and none is when the ladder sits between windows or every rung is spent.
tiers[].pricesobject- This rung's price in every currency it is offered in, minor units keyed by lowercase currency code — the type's own
currency→price_centsalways, plus each extra currency the organizer priced the rung in. A currency the event sells in but this rung has no key for is one the type is not offered in while this rung is live. bandsarray of TicketBand- The ticket's volume ladder: the price drops as the order gets bigger, and the WHOLE order takes one band's price rather than a graduated mix. Absent entirely for a ticket that has none, which is most of them.
Unlike
tiers, this does NOT moveprice_cents: a tier's price is what everybody pays right now, so the type reports it, while a band's price is what a big enough order pays. Soprice_centsis always what ONE ticket costs, and a caller pricing a basket resolves the band itself — the last band whosefrom_qtyis at or below the quantity, elseprice_cents. A caller that skips this and multipliesprice_centsby six will quote a figure checkout does not charge.The quantity that selects a band is the number of THIS type in ONE order, counting only the tickets being paid for — a ticket earned free by a group discount does not count towards a band.
Never present alongside
tiers: a ticket has one ladder or the other. bands[].from_qtyinteger- The order quantity this band starts at — two or more, always. One ticket is the type's own price_cents.
bands[].price_centsinteger- What ONE ticket costs from from_qty upwards. Integer cents; see the type's currency.
bands[].pricesobject- This band's price in every currency it is offered in, minor units keyed by lowercase currency code — the type's own
currency→price_centsalways, plus each extra currency the organizer priced the band in. A currency the type is sold in but this band has no key for is one the band is not offered in: an order in that currency pays the band above it. positioninteger- This type's place on the event page, in the one position space it shares with the event's goods and donations — so a caller rebuilding the page can tell that a t-shirt sits between two tickets rather than after all of them. Lower comes first. The list is already returned in this order; the number is here because array order alone cannot say how a ticket relates to a good.
section_idinteger- The section of the event page this type renders under — one of the sections listed on GET /events/{slug}. This is where the page actually puts it, not the raw stored column: a type nobody has placed by hand falls to the section that claims its kind.
companion.strictnessstringrecommendedshows people a tip and never blocks;requiredrefuses a checkout that falls short — unless the companion is sold out, in which case the rule pauses so this type stays sellable.- one of
recommendedrequired
{
"id": 1,
"name": "Early Bird",
"price_cents": 14900,
"currency": "eur",
"prices": {
"eur": 14900,
"gbp": 12900
},
"on_sale": true,
"quantity": 200,
"sold": 142,
"tiers": [
{
"id": 1,
"name": "First 100",
"price_cents": 12900,
"quantity": 100,
"sold": 100,
"starts_at": null,
"ends_at": "2026-06-01T00:00:00Z",
"live": false,
"prices": {
"eur": 14900,
"gbp": 12900
}
}
],
"bands": [
{
"from_qty": 3,
"price_cents": 8000,
"prices": {
"eur": 8000,
"gbp": 6900
}
}
],
"position": 1,
"section_id": 1,
"companion": {
"ticket_type_id": 2,
"quantity": 1,
"per_quantity": 1,
"strictness": "recommended"
}
}
The OrderItem object
One run of ticket seats. Tickets only — goods, donations and post-order adjustments report through lines (OrderLine).
Attributes
{
"ticket_type": "Early Bird",
"quantity": 2,
"unit_price_cents": 14900
}
The Answer object
One answered question, or one piece of metadata the organizer recorded. The same shape wherever a value appears — on an order, on one of the things bought, or on a ticket. field_id spans both: a question and a detail are one kind of record, and the organizer's Questions and Metadata sections are two views of it.
Attributes
field_idintegerkeystring- The stable machine handle: unique, and fixed when the question or detail was created. Match on this rather than on
label, which the organizer may rewrite any afternoon. labelstring- The question, or the name of the detail, as the organizer wrote it, in the account's own language. Per-locale overlays are for people reading a page; a response has no reader whose language it could mean.
kindstring- How it was asked, and so how to read
value: "text", "textarea", "select", "multi_select", "checkbox", "yes_no", "number", "date", "phone" or "file". valuestring- The answer exactly as stored, never as displayed: a yes/no reads "yes" or "no" rather than the word a page would show it as, a ticked checkbox reads "yes", a multi-select is its choices as one comma-separated list, and a phone number is the digits as given. A question left blank is omitted from the list rather than reported as an empty answer. On a "file" question this names what was uploaded; the bytes themselves are not served by the API.
{
"field_id": 3,
"key": "dietary",
"label": "Dietary requirements",
"kind": "text",
"value": "Vegetarian"
}
The SyncStanding object
One connected service's standing with one ticket — the same ledger the admin attendee page's chip and the workflow run log both read.
Attributes
{
"service": "example",
"remote_id": "example",
"state": "synced",
"synced_at": "2026-10-14T09:00:00Z",
"error": "example"
}
The ItemAnswers object
One of the things bought, and what the buyer said about that one — a question can be attached to a good and asked once per one of them bought, which is how each t-shirt gets its own size.
Attributes
product_idintegerpositioninteger- Which of this product's units this is: zero-based, and counted per PRODUCT across the whole order rather than per line item, because a good split across price bands is still one list of t-shirts to the person buying them. Position 0 is the one the buyer's own order page labels "T-shirt 1".
answersarray of Answer- This item's answers, in a stable order. Never empty — an item with nothing answered about it has no entry at all.
answers[].field_idintegeranswers[].keystring- The stable machine handle: unique, and fixed when the question or detail was created. Match on this rather than on
label, which the organizer may rewrite any afternoon. answers[].labelstring- The question, or the name of the detail, as the organizer wrote it, in the account's own language. Per-locale overlays are for people reading a page; a response has no reader whose language it could mean.
answers[].kindstring- How it was asked, and so how to read
value: "text", "textarea", "select", "multi_select", "checkbox", "yes_no", "number", "date", "phone" or "file". answers[].valuestring- The answer exactly as stored, never as displayed: a yes/no reads "yes" or "no" rather than the word a page would show it as, a ticked checkbox reads "yes", a multi-select is its choices as one comma-separated list, and a phone number is the digits as given. A question left blank is omitted from the list rather than reported as an empty answer. On a "file" question this names what was uploaded; the bytes themselves are not served by the API.
{
"product_id": 1,
"product": "Early Bird",
"position": 1,
"answers": [
{
"field_id": 3,
"key": "dietary",
"label": "Dietary requirements",
"kind": "text",
"value": "Vegetarian"
}
]
}
The OrderLine object
One priced row of an order's ledger — a ticket, a good, a donation, or a post-order adjustment such as an upgrade.
Attributes
product_idinteger- The catalog row this line sold. The same handle
item_answersis keyed on, so what the buyer said about each of these joins to the line that sold them. 0 when no product row backs the line (a ticket type predating the product backfill, whose seats are read out of order_items instead). productstring- The product's name. A ticket line carries the live ticket type name, so it agrees with the matching
itemsentry. quantityintegertax_centsinteger- Integer cents. The tax snapshot taken when the line was written; 0 when the line carries no tax.
total_centsinteger- Integer cents. Subtotal plus an exclusive rate's tax; the subtotal alone for an inclusive one.
statusstring- "active", "removed" or "refunded". Only active lines count towards money; removed and refunded lines are reported rather than dropped, so an order can be reconciled.
- one of
activeremovedrefunded tier_idinteger- The price tier this line was SOLD at. Absent when the ticket has no price ladder. It is reported because a caller cannot work it out after the fact: which rung was live is decided by the clock and by what that rung had left, and by the time an old order is read the ladder has moved on. A tier the organizer removed after it had sold tickets is hidden from the ticket type's
tiers, so a line can name a tier that is no longer listed there — the organizer's Sales report is where it is still named. band_idinteger- The volume band this line was CHARGED at. Absent both when the ticket has no volume ladder and when the buyer paid the ticket's own price — the ladder's first rung, which has no band of its own. It is reported rather than left to be re-derived because a ladder is editable: the band that priced these seats may not exist any more, and resolving
quantityagainst today's ladder would name a rung the buyer was never charged at. Matches an entry of the ticket type'sbandswhile the band is still on sale. ticket_refstring- The ticket this line hangs off — an upgrade, or something bought for one person after the fact. Absent when the line hangs off no ticket, which is every ordinary ticket line.
{
"product_id": 1,
"product": "Early Bird",
"kind": "ticket",
"quantity": 2,
"unit_price_cents": 14900,
"subtotal_cents": 29800,
"tax_cents": 0,
"total_cents": 29800,
"currency": "eur",
"status": "active",
"tier_id": 0,
"band_id": 0,
"ticket_ref": ""
}
The OrderDiscount object
One promotion code redeemed against an order. Not a line: a discount names no product and has no quantity or tax, and the ticket lines it discounted keep their full price. Subtract these from the active lines totals to reach the order's amount_cents.
Attributes
codestring- The promotion code as the buyer typed it, recorded on the redemption — so it still reads back after the code itself has been renamed or deleted.
amount_centsinteger- What this code took off the order, in integer minor units. POSITIVE, and subtracted: the buyer's own receipt renders it as a negative row, while the API reports the amount as the redemption holds it.
{
"code": "EARLYBIRD",
"amount_cents": 2000,
"currency": "eur"
}
The OrderPriceAdjustment object
A price an organizer set for a whole order when they created it by hand, recorded as the difference against what the order's lines come to. Not a line: it names no product and has no quantity or tax, and the lines it adjusts keep their own prices. Not a discount either, which is why it is reported separately: discounts entries are magnitudes that always reduce an order, while this one is signed and can add to it.
Attributes
labelstring- What the organizer called the difference, for example "Sponsor rate". This is the text the buyer reads beside the amount on their payment page and their receipt.
amount_centsinteger- The difference against the order's lines, in integer minor units. SIGNED and added: negative took money off the order, positive added to it.
{
"label": "Speaker discount",
"amount_cents": -5000,
"currency": "eur"
}
The Order object
One purchase. A pending order holds stock from the moment checkout starts; paid, canceled and refunded orders are the history of that purchase.
Attributes
refstring- Short reference printed on receipts and read out at the desk. Unique across the account.
legacy_refstring- The ref this order carried on the platform it was migrated from; empty for orders sold on Tito. It is not always the same as
ref: refs are unique per account here and were unique per event on the source, so a migrated order whose code was already in use was renumbered on import. This field is what the buyer's original confirmation says, and what an integration keyed on the old platform reconciles against. statusstring- The order's state. Pending orders hold stock until paid or expired.
- one of
pendingpaidcanceledrefunded created_atstring (RFC 3339) or null- When the order was started; for a pending order, when checkout began.
itemsarray of OrderItem- The order's TICKET seats, in insertion order — not its whole contents; see
lines. Always an array, never null — an order with no ticket items serializes as[]. items[].ticket_typestring- The ticket type's name, joined in so the raw ticket_type id is never exposed.
linesarray of OrderLine- The order's whole priced ledger, in the order it was written: tickets, goods, donations and post-order adjustments. The active lines'
total_centssum to what the order was charged BEFORE any promotion code came off it — a redeemed code never touches a line, and is reported indiscountsinstead. Subtract those to reachamount_cents. Overlapsitemson tickets by design: a ticket seat appears in both. Always an array, never null. lines[].product_idinteger- The catalog row this line sold. The same handle
item_answersis keyed on, so what the buyer said about each of these joins to the line that sold them. 0 when no product row backs the line (a ticket type predating the product backfill, whose seats are read out of order_items instead). lines[].productstring- The product's name. A ticket line carries the live ticket type name, so it agrees with the matching
itemsentry. lines[].quantityintegerlines[].tax_centsinteger- Integer cents. The tax snapshot taken when the line was written; 0 when the line carries no tax.
lines[].total_centsinteger- Integer cents. Subtotal plus an exclusive rate's tax; the subtotal alone for an inclusive one.
lines[].statusstring- "active", "removed" or "refunded". Only active lines count towards money; removed and refunded lines are reported rather than dropped, so an order can be reconciled.
- one of
activeremovedrefunded lines[].tier_idinteger- The price tier this line was SOLD at. Absent when the ticket has no price ladder. It is reported because a caller cannot work it out after the fact: which rung was live is decided by the clock and by what that rung had left, and by the time an old order is read the ladder has moved on. A tier the organizer removed after it had sold tickets is hidden from the ticket type's
tiers, so a line can name a tier that is no longer listed there — the organizer's Sales report is where it is still named. lines[].band_idinteger- The volume band this line was CHARGED at. Absent both when the ticket has no volume ladder and when the buyer paid the ticket's own price — the ladder's first rung, which has no band of its own. It is reported rather than left to be re-derived because a ladder is editable: the band that priced these seats may not exist any more, and resolving
quantityagainst today's ladder would name a rung the buyer was never charged at. Matches an entry of the ticket type'sbandswhile the band is still on sale. lines[].ticket_refstring- The ticket this line hangs off — an upgrade, or something bought for one person after the fact. Absent when the line hangs off no ticket, which is every ordinary ticket line.
discountsarray of OrderDiscount- The promotion codes redeemed against this order, in the order they were applied — what came OFF it, where
linesis what went on. This is one of the two terms that make the total reconcilable, alongsideprice_adjustments. Almost always empty; always an array, never null. discounts[].codestring- The promotion code as the buyer typed it, recorded on the redemption — so it still reads back after the code itself has been renamed or deleted.
discounts[].amount_centsinteger- What this code took off the order, in integer minor units. POSITIVE, and subtracted: the buyer's own receipt renders it as a negative row, while the API reports the amount as the redemption holds it.
price_adjustmentsarray of OrderPriceAdjustment- A price the organizer set for this whole order, if they set one. Signed and added, so the full identity is: the active
linestotals, less thediscountsamount_cents, plus theseamount_cents, equal the order'samount_cents. Empty on every order nobody hand-priced, which is almost all of them; always an array, never null. price_adjustments[].labelstring- What the organizer called the difference, for example "Sponsor rate". This is the text the buyer reads beside the amount on their payment page and their receipt.
price_adjustments[].amount_centsinteger- The difference against the order's lines, in integer minor units. SIGNED and added: negative took money off the order, positive added to it.
{
"ref": "H7K2PX",
"legacy_ref": "",
"status": "paid",
"email": "ada@example.com",
"name": "Ada Lovelace",
"amount_cents": 29800,
"currency": "eur",
"created_at": "2026-05-18T14:02:11Z",
"paid_at": "2026-05-18T14:03:07Z",
"refunded_at": null,
"items": [
{
"ticket_type": "Early Bird",
"quantity": 2,
"unit_price_cents": 14900
}
],
"lines": [
{
"product_id": 1,
"product": "Early Bird",
"kind": "ticket",
"quantity": 2,
"unit_price_cents": 14900,
"subtotal_cents": 29800,
"tax_cents": 0,
"total_cents": 29800,
"currency": "eur",
"status": "active",
"tier_id": 0,
"band_id": 0,
"ticket_ref": ""
}
],
"discounts": [
{
"code": "EARLYBIRD",
"amount_cents": 2000,
"currency": "eur"
}
],
"price_adjustments": [
{
"label": "Speaker discount",
"amount_cents": -5000,
"currency": "eur"
}
]
}
The CreatedOrder object
What a machine checkout answers with. Deliberately small: the ref to quote, what it costs, where it stands, and the one URL the buyer needs. Read the order back through GET /orders/{ref} for its full shape.
Attributes
statusstring- "pending" for a payable order awaiting payment, "paid" for a zero-total order (fulfilled on creation). A replay reports whatever the order is now, canceled or refunded included.
- one of
pendingpaidcanceledrefunded payment_urlstring- Absolute URL of the buyer's pay page. Present only while the order is pending. A capability URL: treat it as a secret.
urlstring- Absolute URL of the buyer's order page. Present once the order is no longer pending (a free order, or a replay of one since paid). A capability URL: treat it as a secret.
{
"ref": "H7K2PX",
"status": "pending",
"total_cents": 29800,
"currency": "eur",
"payment_url": "https://annual-conf.example/annual-conf/orders/…/pay",
"url": "https://annual-conf.example/annual-conf/orders/…"
}
The Ticket object
One admission, produced when an order is paid. Held by an attendee, who may differ from the buyer.
Attributes
legacy_refstring- The ref this ticket carried on the platform it was migrated from; empty for tickets sold on Tito. See the same field on Order.
answersarray of Answer- What this ticket's holder was asked, plus any metadata the organizer records against them. Always an array, never null — a ticket with neither serializes as
[]. Includes the metadata the organizer keeps to their own team: this read is gated by attendees.view, the very capability that gates the admin's own attendee page.A question can be asked because of the ticket type OR because of a session the ticket got this person into. An organizer can attach a question to a session offering and narrow it to some of its options, so 'what's your level of Rust?' is asked only of the people who picked the Rust workshop. Those answers arrive here, in this same array and this same shape — there is no separate field and nothing says which of the two asked it. Two holders of the same ticket type can therefore carry different questions, and moving somebody to another option drops the answers the new option does not ask for.
answers[].field_idintegeranswers[].keystring- The stable machine handle: unique, and fixed when the question or detail was created. Match on this rather than on
label, which the organizer may rewrite any afternoon. answers[].labelstring- The question, or the name of the detail, as the organizer wrote it, in the account's own language. Per-locale overlays are for people reading a page; a response has no reader whose language it could mean.
answers[].kindstring- How it was asked, and so how to read
value: "text", "textarea", "select", "multi_select", "checkbox", "yes_no", "number", "date", "phone" or "file". answers[].valuestring- The answer exactly as stored, never as displayed: a yes/no reads "yes" or "no" rather than the word a page would show it as, a ticked checkbox reads "yes", a multi-select is its choices as one comma-separated list, and a phone number is the digits as given. A question left blank is omitted from the list rather than reported as an empty answer. On a "file" question this names what was uploaded; the bytes themselves are not served by the API.
syncarray of SyncStanding- Where this ticket stands with each connected service. Always an array, never null — a ticket never sent to any service serializes as
[]. sync[].remote_idstring- The id the service knows this ticket by. Empty when nothing has synced yet.
sync[].synced_atstring (RFC 3339) or null- When the last successful sync completed; null when it has never succeeded.
sync[].errorstring- The service's own words for its last failure, verbatim. Empty when the last attempt succeeded.
{
"ref": "T9QW4M",
"legacy_ref": "",
"status": "valid",
"ticket_type": "Early Bird",
"holder_name": "Ada Lovelace",
"holder_email": "ada@example.com",
"order_ref": "H7K2PX",
"created_at": "2026-05-18T14:03:07Z",
"answers": [
{
"field_id": 3,
"key": "dietary",
"label": "Dietary requirements",
"kind": "text",
"value": "Vegetarian"
}
],
"sync": [
{
"service": "example",
"remote_id": "example",
"state": "synced",
"synced_at": "2026-10-14T09:00:00Z",
"error": "example"
}
]
}
The WidgetSummary object
Attributes
idintegerslugstring- What the pasted markup names. Unique across the ACCOUNT, not per event, so the snippet can carry the widget's name and nothing else.
shapestring- Lives on the record, never in the pasted markup — changing it does not change the organiser's website.
- one of
listbutton secured_lineboolean- Whether the widget shows the “Secured by Tito” line. On by default, switchable.
ticket_type_idsarray of integer- The ticket types this widget lists, in its own order. Empty means it lists everything on sale.
installedboolean- Whether the widget has ever rendered on an approved website. Once true it never returns to false.
installed_atstring (RFC 3339) or nullclosedboolean- Whether the organiser has switched this widget off. A closed widget still answers on their website and says tickets are not on sale, because an empty box there reads as a broken widget.
closed_atstring (RFC 3339) or nullcreated_atstring (RFC 3339)
{
"id": 1,
"slug": "main",
"name": "Homepage list",
"shape": "list",
"secured_line": false,
"ticket_type_ids": [
1
],
"installed": true,
"installed_at": "2026-04-10T10:00:00Z",
"closed": false,
"closed_at": null,
"created_at": "2026-04-01T09:00:00Z"
}
The WidgetDomain object
Attributes
idintegerdomainstring- Normalised host: lowercase, no scheme, no port, no leading www. Approving the bare form covers the www form too, so one row is one website.
statusstring- pending = seen, nobody has decided yet. Only approved renders.
- one of
pendingapprovedblocked sightingsintegerfirst_seen_atstring (RFC 3339)last_seen_atstring (RFC 3339)
{
"id": 1,
"domain": "annualconf.example",
"status": "approved",
"sightings": 318,
"first_seen_at": "2026-04-10T10:00:00Z",
"last_seen_at": "2026-08-27T18:12:00Z"
}
The SavedReport object
Attributes
querystring- The whole definition, as a URL query string — grouping, date range, what it measures against, and any filters. Empty means the report's defaults (grouped by day, last 30 days, measured against the day an order was paid).
created_atstring (RFC 3339)- Integer Unix timestamp in storage, rendered as RFC 3339 UTC. Not nullable (NOT NULL column).
updated_atstring (RFC 3339)- When the name or the definition last changed. Equals
created_aton a report nobody has edited.
{
"id": 1,
"name": "Sales by week",
"source": "orders",
"query": "group=week\u0026range=all",
"created_at": "2026-05-01T09:00:00Z",
"updated_at": "2026-05-01T09:00:00Z"
}
The CheckinListSummary object
A check-in list and its live count.
Attributes
created_atstring (RFC 3339)- Integer Unix timestamp in storage, rendered as RFC 3339 UTC. Not nullable — a list always has a creation time (NOT NULL column).
checked_in_countinteger- Check-ins recorded against this list, counted exactly as the admin's door page counts them: only tickets that are still valid (voided tickets drop out), and on an entitlement-scoped list only tickets holding that entitlement. 0 when the list has no check-ins yet, never a missing/null value.
{
"id": 1,
"name": "Main entrance",
"created_at": "2026-09-01T08:00:00Z",
"checked_in_count": 412
}
The Checkin object
One recorded arrival.
Attributes
{
"ticket_ref": "T9QW4M",
"checked_in_at": "2026-10-14T08:41:19Z"
}
The Workflow object
Attributes
idintegernamestringenabledbooleanall_eventsboolean- Whether this workflow fires for every event, including ones created later (the workflow_subscriptions event_id=0 sentinel). When true, events is always [].
created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or nulleventsarray of string- Slugs of the events subscribed to this workflow. Always an array, never null, when the workflow has none.
definitionobject- The workflow's node/edge graph (internal/workflow.Definition), verbatim as stored — organizer-authored, no secrets. Present only on the single-workflow endpoint, and only when the stored definition parses as JSON; see definition_raw.
definition_rawstring- The raw, unparsed definition text. Present instead of definition, on the single-workflow endpoint only, when the stored JSON fails to parse — a corrupt definition must not break this endpoint.
{
"id": 1,
"name": "Thank-you email",
"enabled": true,
"all_events": true,
"created_at": "2026-04-01T09:00:00Z",
"updated_at": "2026-04-01T09:00:00Z",
"events": [
"example"
],
"definition": {},
"definition_raw": "example"
}
The WorkflowRun object
Attributes
idintegerstatusstring- "pending", "running", "completed", or "failed". A run that fails on something worth retrying — a 5xx, a 429, or no answer at all — goes back to "pending" for its next attempt rather than straight to "failed".
- one of
pendingrunningcompletedfailed attemptinteger- How many attempts have been made. A "pending" run with attempt above 0 is waiting on a retry rather than queued for the first time. Retries back off a minute, five, half an hour, then two hours; after that the run fails for good.
created_atstring (RFC 3339) or nulllogarray of any- The run's step log (internal/workflow.Step array), verbatim as stored. Present only on the single-run endpoint. Falls back to a JSON string of the raw stored text when it fails to parse as JSON — the log is a debugging surface and must not break this endpoint on a malformed row.
{
"id": 1,
"trigger": "order.paid",
"subject_type": "order",
"subject_id": 1,
"status": "completed",
"attempt": 1,
"created_at": "2026-05-18T14:03:08Z",
"started_at": "2026-05-18T14:03:08Z",
"finished_at": "2026-05-18T14:03:09Z",
"log": [
null
]
}
The CompanionRule object
A companion rule in the direction the organizer authored it: an order holding this ticket type needs quantity of ticket_type_id for every per_quantity of this one. The requirement always rounds up — four of this type at 1-per-3 needs two companions. Counted within a single order. API checkout is never blocked by a companion rule; this is reported so a caller can show the same requirement the event page does.
Attributes
strictnessstringrecommendedshows people a tip and never blocks;requiredrefuses a checkout that falls short — unless the companion is sold out, in which case the rule pauses so this type stays sellable.- one of
recommendedrequired
{
"ticket_type_id": 2,
"quantity": 1,
"per_quantity": 1,
"strictness": "recommended"
}
The ReportRow object
One grouped row of the custom report — and, in totals, the same shape summing every row. Amounts are integer minor units of currency (never a float), aggregated at order grain.
Attributes
keystring- The bucket's machine-stable id: an ISO date (
dayis2026-08-05,weekthe Monday it starts,month2026-08), an audience's flow slug, a status, a discount code, or a lowercase currency code. Legitimately EMPTY for the main page's audience, an order with no discount code, and the totals line. labelstring- The same bucket worded for a human. Always English here, like every other machine-facing string in this API — the admin page localizes it; a caller with its own wording should read
key. gross_centsinteger- What changed hands, before refunds — the orders' own amounts, so discounts are already applied and non-ticket items are included.
tax_centsinteger- Tax to remit on those orders, as a positive amount: the tax snapshotted on each order line when it was sold, summed over the lines still standing. An order refunded in full contributes nothing, and a ticket refunded off an order takes its own line's tax with it. NOT subtracted from
net_cents— see/events/{slug}/incomefor the ledger that walks it down. currencystring- The event's currency. Orders taken in another currency are still summed into these amounts — group by
currencyto split them apart.
{
"key": "2026-W20",
"label": "Week of 11 May",
"orders": 38,
"tickets": 61,
"gross_cents": 909000,
"discount_cents": 20000,
"refund_cents": 14900,
"tax_cents": 0,
"net_cents": 874100,
"currency": "eur"
}
The IncomeMonth object
One month of the income ledger. The months decompose the ledger exactly — column by column they sum to the top-level totals. Amounts are integer minor units of the event's currency, and deductions and tax are POSITIVE magnitudes: the admin page draws them with a minus because it is walking a figure down, but a caller doing arithmetic wants the amount and the operation stated separately.
Attributes
labelstring- The same month worded for a human. Always English here, like every other machine-facing string in this API.
gross_centsinteger- Before anything comes off: what the orders charged plus the discount that came off them.
fees_centsinteger- What the payments cost, as a positive amount: the provider's cut and Tito's together. The ledger's own totals split the two.
{
"key": "2026-05",
"label": "May 2026",
"gross_cents": 909000,
"deductions_cents": 34900,
"tax_cents": 0,
"fees_cents": 18180,
"net_cents": 855920
}
The Series object
An account-level grouping of events with a listing page of its own.
Attributes
{
"slug": "annual-conference",
"name": "Annual Conference",
"intro": "Every autumn since 2019.",
"listed": true,
"show_past": true,
"event_count": 7,
"created_at": "2019-03-02T11:20:41Z"
}
The WebhookEndpoint object
One outbound-webhook subscription. Never carries its signing secret — that is admin-only data, shown once per visit on the Settings → Webhooks detail page, never in this API.
Attributes
idintegerevent_typesarray of string- The event types this endpoint is subscribed to — a non-empty subset of the fixed v1 vocabulary.
enabledboolean- False either because an admin turned it off, or because the drainer auto-disabled it after 10 consecutive permanently-failed deliveries (distinguish the two by
disabled_at: auto-disable always stamps it, a manual toggle never does). consecutive_failuresinteger- Consecutive permanently-failed deliveries (each having exhausted its own retry schedule) since the last success. Resets to 0 on the next successful delivery, or when the endpoint is re-enabled.
created_atstring (RFC 3339)disabled_atstring (RFC 3339) or null- Null unless the drainer auto-disabled this endpoint (10 consecutive permanently-failed deliveries) — a manual disable never sets this, which is what makes its presence the auto-disabled signal.
{
"id": 1,
"url": "https://example.com/hooks/tito",
"event_types": [
"order.created"
],
"enabled": true,
"consecutive_failures": 0,
"created_at": "2026-04-01T09:00:00Z",
"disabled_at": null
}
The WebhookDelivery object
One delivery attempt record — queue row and audit-trail row at once, exactly as the Settings → Webhooks detail page shows it.
Attributes
event_typestring- Also the value sent as the Tito-Webhook-Event header on the wire. "webhook.test" is a test delivery sent by hand from the admin rather than a real account event, so it is not a type an endpoint can subscribe to.
statusstring- "pending" is still retrying (or awaiting its first attempt); "succeeded" got a 2xx response; "failed" exhausted the 6-attempt retry schedule without one.
- one of
pendingsucceededfailed next_attempt_atstring (RFC 3339)- When the next attempt is due, for a delivery still on the retry schedule (6 attempts spread over about ten and a half hours). In the past for one that is due now; carries no meaning once status leaves "pending".
last_status_codeinteger- The HTTP status the receiver answered on the most recent attempt. 0 when no attempt has completed yet, or every attempt so far failed at the transport level (refused/unreachable target, timeout) rather than getting an HTTP response.
last_errorstring- The most recent attempt's failure reason (transport error, or "endpoint answered <code>" for a non-2xx response), truncated to 500 characters. Empty once a delivery has succeeded.
created_atstring (RFC 3339)delivered_atstring (RFC 3339) or null- Null until this delivery succeeds; set once, on the attempt that got a 2xx.
{
"id": 1,
"event_type": "order.paid",
"status": "succeeded",
"attempts": 1,
"next_attempt_at": "",
"last_status_code": 200,
"last_error": "",
"created_at": "2026-05-18T14:03:08Z",
"delivered_at": "2026-05-18T14:03:09Z"
}