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 API is in beta. Endpoints and fields can change without a new version number, and a change can break an integration built before it.
This is the generic copy of the reference. Every account has its own at https://go.tito.io/<yourslug>/docs/api, for the people in that account, with its 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": [
{
"admin_url": "https://go.tito.io/youraccount/annual-conf/admin",
"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",
"ends_at": "2026-10-15T17:00:00Z",
"all_day": false,
"date_text": "",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"updated_at": "2026-03-09T16:04:12Z",
"draft": false,
"secret": false,
"unlisted": false,
"archived": false,
"archived_at": "2026-03-09T16:04:12Z",
"delete_on": "2026-11-10T14:30:00Z",
"anonymize_on": "2026-11-10T14:30:00Z",
"search_indexing": "inherit",
"no_index": false,
"share_title": "",
"share_text": "",
"share_image_url": "/a/8f2c1d9e4b7a6350.png",
"passed_params_on": true,
"passed_params_pattern": "utm_*",
"calendar_on_event_page": false,
"secret_redirect_url": "https://example.com/tickets",
"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"
}
}
Apps that connect with OAuth
A key belongs to one account, and somebody on it has to paste it in. If you're building something many Tito accounts will use, register an app instead. Each organizer connects it from their own account, chooses what it can do, and can disconnect it whenever they like. It's OAuth 2.1: the authorization code flow with PKCE.
Register the app
In your own Tito account, go to Settings → API → Your apps. Give the app a name, a website, up to ten redirect addresses, and every permission it may ever ask for. A redirect address starts with https://, or http:// on localhost, 127.0.0.1 or [::1] with any port while you build. You get a client ID, and a secret that is shown once. Only the account's owners can register an app.
Until Tito verifies the app, organizers see Not verified by Tito when they connect it, and each connection lasts 30 days. Ask for a review from the app's page. Once it is verified, the organizer chooses how long it stays connected: 30 days, which is the default, or until they disconnect it.
An AI agent or MCP client that belongs to nobody's account doesn't need any of this. It can register itself at https://go.tito.io/auth/agent/register, with no secret, as the Tito skill describes.
Connect an account
- Ask the organizer for their account's address,
https://go.tito.io/their-account. Send their browser to/connect-agenton it, with your client ID, a redirect address you registered, astate, and a PKCEcode_challenge(S256 only). To ask for particular permissions, addscope: the capability names from Authentication, separated by spaces. Leave it out and the organizer chooses between read-only and everything their role can do. - They sign in, see your app and what it asks for, and approve it.
- Tito sends them back to your redirect address with
code,stateandiss. Check thatstateis yours andissishttps://go.tito.io. Instead of a code you may get anerror:access_deniedif they said no,invalid_scopeif you asked for a permission their role doesn't have or your app didn't declare, andinvalid_requestfor a malformed request. - Within five minutes, exchange the code at
https://go.tito.io/auth/agent/token. Send your secret with HTTP Basic authentication, or asclient_idandclient_secretin the body. Includeresource, the account's address followed by/api/mcp, exactly as shown. It names the account the connection is for, whichever of its APIs you then call. - Call the API with the access token as a bearer token, at
https://go.tito.io/their-account/api/v1. Tokens are only accepted there, not on the account's own host, which takes keys.
Keep it connected
An access token lasts 15 minutes, or less near the end of a connection, so go by expires_in. Refresh it at the same token endpoint. You can leave out resource when you refresh. Every refresh returns a new refresh token: store it and throw the old one away. Presenting a refresh token that has already been used ends the connection, because that is what a stolen one looks like.
A connection ends when the person who connected it, or one of the account's owners, disconnects it, when that person leaves the account, when the account's sign-in settings change, when it reaches its 30 days, or after 13 months without an API call. It also ends when you delete the app or Tito suspends it, and a verified connection ends if Tito withdraws the app's approval. After that, calls get 401 with invalid_token. The token endpoint answers 400 with invalid_grant, or 401 with invalid_client when the app itself is deleted or suspended. Send the organizer through the connection again.
What a connection can do follows the person who connected it. If their role changes, calls outside their new role get the same 403 missing_capability a key gets.
To disconnect itself, an app revokes its refresh token at https://go.tito.io/auth/agent/revoke, sending it as token (RFC 7009) and authenticating with its secret as at the token endpoint.
The token endpoint allows 30 tries of the same code or refresh token from one address in 15 minutes, and answers 429 after that. Don't retry an exchange that failed with invalid_grant.
Everything above is also published as server metadata at https://go.tito.io/.well-known/oauth-authorization-server (RFC 8414), including every capability name in scopes_supported.
https://go.tito.io/their-account/connect-agent
?response_type=code
&client_id=your_client_id
&redirect_uri=https://yourapp.example/callback
&state=random_state
&code_challenge=base64url(sha256(verifier))
&code_challenge_method=S256
&scope=events.view%20orders.view
curl https://go.tito.io/auth/agent/token \
-u "your_client_id:your_client_secret" \
-d grant_type=authorization_code \
-d code=the_code \
--data-urlencode redirect_uri=https://yourapp.example/callback \
--data-urlencode resource=https://go.tito.io/their-account/api/mcp \
-d code_verifier=the_verifier
{
"access_token": "tito_agent_access.…",
"refresh_token": "tito_agent_refresh2.…",
"token_type": "Bearer",
"expires_in": 900,
"scope": "events.view orders.view"
}
curl https://go.tito.io/their-account/api/v1/events \
-H "Authorization: Bearer tito_agent_access.…"
curl https://go.tito.io/auth/agent/token \
-u "your_client_id:your_client_secret" \
-d grant_type=refresh_token \
-d refresh_token=tito_agent_refresh2.…
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 on the API itself yet, only on connecting apps (OAuth). 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.
General
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.
search_indexingboolean- Whether this account has opened its public site to search engines. False until an organizer turns it on, which is where every account starts, and while it is false nothing on the site is listed anywhere: the account's event list, its series pages and every event page all carry a noindex instruction. It closes nothing and gates nothing. An event with search_indexing "inherit" — the default — follows this value, so moving it moves every such event with it.
home_pagestring- What "/" shows on the account's own host:
events,event:<slug>orpage:<path>. An unreadable stored value answers asevents, the way the door itself reads it. root_pagesboolean- Whether the account's pages answer on the root domain as well. ABSENT on an instance with no root domain — there is nothing for it to be true or false about. Unset reads as true: the root domain's URLs are the default surface.
close_onstring (RFC 3339) or null- When the account is due to be closed, 30 days after its owner asked for it in Account settings. Null when it isn't closing. Asking and keeping the account happen in the admin only: asking emails the owner and every admin.
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",
"search_indexing": false,
"home_page": "events",
"root_pages": true,
"close_on": null
}
Change the account's general settings
PATCH /account
The account's own set-up: what it is called, whether search engines may list its site, what its front door shows, and — where there is a root domain — whether its pages answer there too. Only the keys present change.
Two of these make every public page STALE the moment they move, and both purge the cached pages as part of the write: search_indexing (the noindex instruction rides the cached shells) and home_page (it is what "/" serves). Setting either to what it already is purges nothing.
home_page is events (the account's event listing), event:<slug> or page:<path>, and the target has to exist and be public — a front door pointing at nothing is the one thing this will not store. It governs the account's OWN host; the root domain's /{slug} listing is the standardized surface and is untouched by it.
The admin clock is not here: it is a standalone-instance knob, and the coverage manifest records it as the gap rather than the API offering a field that does nothing on a hosted account.
Requires settings.manage
Request body
search_indexingboolean- Opens or closes the whole public site to search engines. Moving it purges every public page.
home_pagestringevents,event:<slug>orpage:<path>. The target must exist and be public. Moving it purges every public page.
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.
search_indexingboolean- Whether this account has opened its public site to search engines. False until an organizer turns it on, which is where every account starts, and while it is false nothing on the site is listed anywhere: the account's event list, its series pages and every event page all carry a noindex instruction. It closes nothing and gates nothing. An event with search_indexing "inherit" — the default — follows this value, so moving it moves every such event with it.
home_pagestring- What "/" shows on the account's own host:
events,event:<slug>orpage:<path>. An unreadable stored value answers asevents, the way the door itself reads it. root_pagesboolean- Whether the account's pages answer on the root domain as well. ABSENT on an instance with no root domain — there is nothing for it to be true or false about. Unset reads as true: the root domain's URLs are the default surface.
close_onstring (RFC 3339) or null- When the account is due to be closed, 30 days after its owner asked for it in Account settings. Null when it isn't closing. Asking and keeping the account happen in the admin only: asking emails the owner and every admin.
Responses
200The account, in the shapeGET /accountanswers with.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409root_pageswas named on an instance with no root domain to put pages on (no_root_domain).422A blank name, a name over 100 characters, a blank or unknownhome_page, an explicit null, or an unknown field (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/account \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"The Sea Church"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/account", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "The Sea Church"
}),
});
const data = await res.json();
{
"slug": "acme",
"name": "Acme Events",
"region": "uk",
"search_indexing": false,
"home_page": "events",
"root_pages": true,
"close_on": null
}
List the data-protection log
GET /data-protection-log
Every data-protection action on the account, newest first: each person anonymized, and each event deleted or set to be deleted and kept. A line names the record's reference, never the person. Paged by after, an entry id from the previous page's next_after. An agent limited to some events cannot call this.
Requires erasure
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
entriesarray of DataProtectionLogEntryentries[].idintegerentries[].actionstring- one of
ticket.anonymizedorder.anonymizedinvitation.anonymizedbuyer.anonymizedwaitlist.anonymizedapplication.anonymizedevent.deletedevent.delete_requestedevent.delete_keptevent.anonymize_requestedevent.anonymize_keptevent.anonymizedaccount.close_requestedaccount.close_kept entries[].subject_refstring- The record's reference, or the event's id, or empty for the account itself. Never the person.
entries[].eventstring or null- The slug of the event the record belongs to, null for a customer or a deleted event.
entries[].atstring (RFC 3339)
Responses
200A page of the log.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422afteris not an entry id (invalid_after).500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/data-protection-log \
-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/data-protection-log", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"entries": [
{
"id": 1,
"action": "ticket.anonymized",
"subject_type": "ticket",
"subject_ref": "H7K2PX",
"event": "example",
"actor": "example",
"at": "2026-10-14T09:00:00Z"
}
],
"next_after": "example"
}
Get the order-notification settings
GET /notifications
When an order is paid the organizer gets an email, throttled so a rush becomes one summary per period instead of an inbox flood. Notifications are OPT-OUT: on is the resting state and the switch is how they are turned off.
effective_email is the field that answers "is this working?" — where a notification would actually go right now, or "" for nothing sends, whether because the switch is off or because there is no address anywhere. The other three (on, email, default_email) are what it is computed from.
Requires settings.manage
Response fields
emailstring- The STORED address; "" means the instance default applies. Kept even while the switch is off.
default_emailstring- The instance's own fallback address, "" where there is none. Read-only — it is the fleet's, not the account's.
period_secondsinteger- The throttle: at most one email per period. 0 is no throttle — every order reports immediately.
localestring- The language the notification is written in, resolved: the stored one, else the admin language, else English.
Responses
200The notification settings.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/notifications \
-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/notifications", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"on": true,
"email": "orders@example.com",
"default_email": "",
"effective_email": "orders@example.com",
"period_seconds": 900,
"locale": "en"
}
Change the order-notification settings
PATCH /notifications
Only the keys present change.
Switching notifications on starts the clock now. Orders that were paid while they were off are marked reported at the moment of switch-on, so on means "from now" rather than "and here is everything you missed" — a caller flipping this in a script would otherwise mail an organizer a digest of their own history.
email is checked for SYNTAX only, the same bargain PATCH /tickets/{ref} makes: the organizer's own form runs a deliverability check with domain suggestions because a person is standing there to read "did you mean gmail.com?", and a machine has nobody to ask. "" is a real answer meaning "use the instance default", and the stored address is KEPT while the switch is off so turning them back on does not lose it.
period_seconds of 0 is a real answer too — every order reports immediately — and is not the same as leaving the key out. A negative one is refused rather than silently defaulted.
Requires settings.manage
Request body
Response fields
emailstring- The STORED address; "" means the instance default applies. Kept even while the switch is off.
default_emailstring- The instance's own fallback address, "" where there is none. Read-only — it is the fleet's, not the account's.
period_secondsinteger- The throttle: at most one email per period. 0 is no throttle — every order reports immediately.
localestring- The language the notification is written in, resolved: the stored one, else the admin language, else English.
Responses
200The settings, in the shapeGET /notificationsanswers with.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422onwas true with no address stored and no instance default, which is a preference nothing can act on (no_address); oremailis not an address,period_secondsis negative,localeis not a language this build has, or the body carried an explicit null or an unknown field (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/notifications \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"on":true}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/notifications", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"on": true
}),
});
const data = await res.json();
{
"on": true,
"email": "orders@example.com",
"default_email": "",
"effective_email": "orders@example.com",
"period_seconds": 900,
"locale": "en"
}
Customers
List buyer kinds
GET /buyer-kinds
The account's kinds, by name, each with how many buyers have it. A kind is a label for finding and filtering buyers; nothing else depends on it. There is no paging: an account has a handful.
Requires orders.view
Response fields
Responses
200The kinds.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/buyer-kinds \
-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/buyer-kinds", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"buyer_kinds": [
{
"id": 2,
"name": "Partners",
"buyer_count": 12
}
]
}
Create a buyer kind
POST /buyer-kinds
Add a kind. The name is trimmed, and must not be blank or already belong to another kind, however it's cased. Answers with the kind as stored.
Requires settings.manage
Request body
namestring required
Response fields
Responses
201The kind was created.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422The body isn't{"name": "..."}(invalid_body), the name is blank (invalid_name), or another kind already has that name, however it's cased (name_taken). A name is trimmed and kept to 200 characters.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyer-kinds \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Partners"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyer-kinds", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Partners"
}),
});
const data = await res.json();
{
"id": 2,
"name": "Partners",
"buyer_count": 12
}
Remove a buyer kind
DELETE /buyer-kinds/{id}
Remove a kind. Every buyer with it is left with no kind, in the same step; no buyer is removed. Answers with how many buyers were moved.
Requires settings.manage
Path parameters
idinteger required
Response fields
Responses
200The kind was removed.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/buyer-kinds/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/buyer-kinds/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 2,
"deleted": true,
"buyers_reset": 12
}
One buyer kind
GET /buyer-kinds/{id}
One kind, with how many buyers have it.
Requires orders.view
Path parameters
idinteger required
Response fields
Responses
200The kind.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/buyer-kinds/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/buyer-kinds/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 2,
"name": "Partners",
"buyer_count": 12
}
Rename a buyer kind
PATCH /buyer-kinds/{id}
Give a kind a new name, on the same rules as a new one. Every buyer with the kind has it under the new name at once.
Requires settings.manage
Path parameters
idinteger required
Request body
namestring required
Response fields
Responses
200The kind as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body isn't{"name": "..."}(invalid_body), the name is blank (invalid_name), or another kind already has that name, however it's cased (name_taken). A name is trimmed and kept to 200 characters.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/buyer-kinds/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Partners"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyer-kinds/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Partners"
}),
});
const data = await res.json();
{
"id": 2,
"name": "Partners",
"buyer_count": 12
}
The buyer register
GET /buyers
Newest first, searchable by organization, name or email — the same match the organizer's own list makes.
No capability token is served here. A buyer's order page and a quote page are each opened by a crypto-random token, and that token IS the credential — so the summaries below carry a ref and nothing that opens anything. A caller that needs a quote's buyer link asks GET /quotes/{ref}, where serving it is the point.
Requires orders.view
Query parameters
limitinteger- Page size. Default 50, max 200. Non-positive or unparseable values fall back to the default.
afterstring- Keep the same search and sort when continuing from a cursor; an unknown cursor returns 422.
qstring- Match organization, name or email as a substring.
sortstring- Sort by newest registration, organization, or name. Keep this value when using after.
Response fields
buyersarray of BuyerAccountbuyers[].refstringbuyers[].emailstringbuyers[].namestringbuyers[].orgstringbuyers[].phonestring- The buyer's phone number as it was typed, trimmed and cut to 64 characters. It isn't checked or reformatted. Empty when the buyer has none.
buyers[].quote_countinteger- Quotes matching this buyer's id OR their email — an anonymous quote is still theirs.
buyers[].order_countinteger- Orders matching their email, case-insensitively: buyers.email is lowercased on the way in and an order keeps the address as the buyer typed it, so a case-sensitive match once read a registered buyer as having no orders at all.
buyers[].part_ofstring or null- The ref of the buyer this one is part of, like a department's company, or null when it is part of nothing. One level: a buyer others are part of is never part of another.
buyers[].kindobject or null- The kind a buyer is, or null for none. A kind is a label for finding and filtering buyers; nothing else depends on it. The whole kind is
GET /buyer-kinds/{id}. buyers[].kind.idintegerbuyers[].kind.namestringbuyers[].created_atstring (RFC 3339) or nullbuyers[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
buyers[].anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
Responses
200A page of buyers.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422Invalid sort (invalid_sort) or cursor (invalid_after).500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/buyers \
-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/buyers", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"buyers": [
{
"ref": "b7x9k2",
"email": "accounts@example.com",
"name": "Alex Smith",
"org": "Example Ltd",
"phone": "+1 202 555 0143",
"quote_count": 3,
"order_count": 5,
"part_of": "k3m8p1",
"kind": {
"id": 2,
"name": "Partners"
},
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito",
"anonymized_at": null
}
],
"next_after": "example"
}
Add a buyer
POST /buyers
Add one buyer, the same way a row of the buyer import adds one. The email is trimmed and lowercased, and becomes the buyer's primary contact and first person. Name, organization, phone number and billing identity are optional. Nobody is emailed. An address that is already a buyer's primary contact answers 409 buyer_exists, and the message names that buyer's ref. To add someone to an existing buyer, use POST /buyers/{ref}/people.
Requires orders.manage
Request body
billingobject- The new buyer's billing identity, any of the fields.
countrymust be an ISO 3166-1 alpha-2 code or blank. billing.namestring- The legal entity an invoice is addressed to. Blank falls back to
orgwhen an invoice is rendered. billing.line1stringbilling.line2stringbilling.citystringbilling.postcodestringbilling.vat_numberstring
Response fields
refstringemailstringnamestringorgstringphonestring- The buyer's phone number as it was typed, trimmed and cut to 64 characters. It isn't checked or reformatted. Empty when the buyer has none.
quote_countinteger- Quotes matching this buyer's id OR their email — an anonymous quote is still theirs.
order_countinteger- Orders matching their email, case-insensitively: buyers.email is lowercased on the way in and an order keeps the address as the buyer typed it, so a case-sensitive match once read a registered buyer as having no orders at all.
part_ofstring or null- The ref of the buyer this one is part of, like a department's company, or null when it is part of nothing. One level: a buyer others are part of is never part of another.
kindobject or null- The kind a buyer is, or null for none. A kind is a label for finding and filtering buyers; nothing else depends on it. The whole kind is
GET /buyer-kinds/{id}. kind.idintegerkind.namestringcreated_atstring (RFC 3339) or nulladmin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
stripe_customer_idstring- The Customer this buyer is on Stripe, empty until something mints one (the bank-transfer path does it lazily). An identifier, not a credential.
people[].emailstringpeople[].primaryboolean- The buyer's primary contact: the address it is listed under, the one invoices and Stripe use. It can't be removed; make someone else primary first (
POST /buyers/{ref}/primary). people[].rolestring- What this person can do when the buyer's quotes need approval: a requester sends a quote for approval, an approver can reject one. Where approval isn't needed the two can do the same things.
- one of
requesterapprover people[].sees_allboolean- Whether this person sees every one of the buyer's bookings and quotes when they sign in, or only the ones made with their own email address. Only the organizer can change it (
POST /buyers/{ref}/people/{email}/sees). domainsarray of string- The buyer's email domains, sorted, never null. Addresses at exactly these domains belong to the buyer before anybody signs in.
termsobject or null- The named payment terms a buyer has of its own, or null when it has none. A buyer without terms of its own follows the terms of the buyer it is part of (
inherited_termson the buyer), or else each event's own bank transfer settings. The whole set isGET /payment-terms/{id}. terms.idintegerterms.namestringinherited_termsobject- What a buyer without terms of its own follows: the terms of the buyer it is part of. Null when it has its own, is part of nothing, or that buyer has none either.
inherited_terms.idintegerinherited_terms.namestringincludes[].refstringincludes[].namestringpo_requiredboolean- This buyer's own approval setting: its quotes, and those of every buyer part of it, go to an approver before they can become orders. Always false on a buyer that is part of another, which follows that buyer's;
approvalsays what applies. Set it withPOST /buyers/{ref}/po-required. approvalobject- Approval as it applies to this buyer: required or not, and the buyer whose approvers answer, which is the buyer this one is part of, or else itself.
approval.requiredbooleanapproval.approverobjectapproval.approver.refstringapproval.approver.namestringblockobject- Whether the buyer is blocked from starting new orders and quotes, by its own block or the one on the buyer it is part of. Set it with
POST /buyers/{ref}/blockand lift it withDELETE /buyers/{ref}/block. block.blockedbooleanblock.messagestring- The words emailed to whoever at the buyer tries to order, as plain text, empty for the standard line.
block.blocked_atstring (RFC 3339) or null- When the block was set, null when the buyer isn't blocked.
block.byobject or null- The buyer whose block applies: this one, or the one it is part of. Null when the buyer isn't blocked.
block.by.refstringblock.by.namestringspendarray of object- What this buyer has actually PAID, per currency: order totals less whatever was refunded off them. A buyer can span currencies across events, so it is a list. A partial refund leaves the order paid and is netted; a full one flips its status and nets to zero either way.
spend[].centsintegerspend[].currencystringquotesarray of BuyerQuoteSummaryquotes[].refstringquotes[].event_slugstringquotes[].statusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
quotes[].amount_centsintegerquotes[].currencystringquotes[].expires_atstring (RFC 3339) or nullquotes[].created_atstring (RFC 3339) or nullquotes[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
ordersarray of BuyerOrderSummaryorders[].refstringorders[].event_slugstringorders[].statusstringorders[].amount_centsintegerorders[].currencystringorders[].payment_methodstringorders[].created_atstring (RFC 3339) or nullorders[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
bank_transferobject or null- The newest wire-instruction snapshot, null until this buyer has one. Read from the stored row — no Stripe call — because the virtual IBAN is per-Customer, so that row IS the account they pay into. The IBAN is here deliberately: it is the account's own receiving account, printed on the invoice the buyer was already sent, not a secret that opens anything.
bank_transfer.referencestringbank_transfer.ibanstringbank_transfer.bicstringbank_transfer.account_holderstringbank_transfer.bank_countrystringbank_transfer.amount_centsintegerbank_transfer.currencystringbank_transfer.due_atstring (RFC 3339) or nullbank_detailsobject or null- The bank details Stripe issued this buyer's current Stripe customer, kept on the buyer: the account the buyer pays every bank transfer into, and what its vendor form prints. Null until they are kept, either by
POST /buyers/{ref}/bank-detailsor by the buyer's first Stripe bank transfer. Details kept for a Stripe customer the buyer is no longer linked to are never served. Read from the stored row, with no Stripe call. bank_details.account_holderstringbank_details.ibanstringbank_details.bicstringbank_details.currencystringbillingobject- This customer's billing identity, as currently stored (never a computed fallback — a blank
namehere can still print asorgon an invoice). billing.namestring- The legal entity an invoice is addressed to. Blank falls back to
orgwhen an invoice is rendered. billing.line1stringbilling.line2stringbilling.citystringbilling.postcodestringbilling.vat_numberstring
Responses
201The new buyer.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.409The address is already a buyer's primary contact (buyer_exists). The message names that buyer's ref.422The body isn't{"email", "name", "org", "phone", "billing"}(invalid_body), the email is missing or not an email address (invalid_email), orbilling.countryisn't a valid ISO 3166-1 alpha-2 code (invalid_country).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"email":"accounts@example.com"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"email": "accounts@example.com"
}),
});
const data = await res.json();
{
"ref": "b7x9k2",
"email": "accounts@example.com",
"name": "Alex Smith",
"org": "Example Ltd",
"phone": "+1 202 555 0143",
"quote_count": 3,
"order_count": 5,
"part_of": "k3m8p1",
"kind": {
"id": 2,
"name": "Partners"
},
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito",
"anonymized_at": null,
"stripe_customer_id": "cus_1MEXAMPLE",
"people": [
{
"email": "accounts@example.com",
"primary": true,
"role": "approver",
"sees_all": true
}
],
"domains": [
"example.org"
],
"terms": {
"id": 3,
"name": "Members"
},
"inherited_terms": {
"id": 3,
"name": "Members"
},
"includes": [
{
"ref": "p2q7r4",
"name": "Example Ltd Design"
}
],
"po_required": false,
"approval": {
"required": true,
"approver": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
},
"block": {
"blocked": true,
"message": "Please settle your outstanding invoices with our accounts team before booking again.",
"blocked_at": "2026-10-21T11:30:00Z",
"by": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
},
"spend": [
{
"cents": 150000,
"currency": "eur"
}
],
"quotes": [
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "open",
"amount_cents": 14900,
"currency": "eur",
"order_ref": "H7K2PX",
"expires_at": "2026-10-14T09:00:00Z",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
],
"orders": [
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "paid",
"amount_cents": 14900,
"currency": "eur",
"payment_method": "example",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
],
"bank_transfer": {
"reference": "RF97TESTREF",
"iban": "example",
"bic": "example",
"account_holder": "Ada Lovelace",
"bank_country": "example",
"amount_cents": 14900,
"currency": "eur",
"status": "awaiting",
"due_at": "2026-10-14T09:00:00Z"
},
"bank_details": {
"account_holder": "Stripe Payments Europe",
"iban": "IE29AIBK93115212345678",
"bic": "AIBKIE2D",
"bank_country": "IE",
"currency": "eur"
},
"billing": {
"name": "St Brigid's NS Board of Management",
"line1": "12 Main Street",
"line2": "",
"city": "Springfield",
"postcode": "D02 AF30",
"subdivision": "Co. Dublin",
"country": "IE",
"vat_number": "IE1234567T",
"buyer_reference": "",
"po_reference": ""
}
}
Delete a buyer with no quotes or orders
DELETE /buyers/{ref}
Deletes the buyer, with the people and email domains on it. Only a buyer with no quotes, no orders and no Stripe Customer can be deleted, the same rule as the admin's Delete. Anything else answers 409 not_empty; anonymize it instead (POST /buyers/{ref}/anonymize). Buyers that were part of this one stay, each on its own. A blocked buyer's block goes with it.
Requires orders.manage
Path parameters
refstring required
Response fields
Responses
200The buyer was deleted.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The buyer has quotes, orders or a Stripe Customer (not_empty).500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/buyers/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/buyers/H7K2PX", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"ref": "b7x9k2",
"deleted": true
}
Everything for one buyer
GET /buyers/{ref}
The support answer: what this buyer has spent, per currency and net of refunds, with their quotes and orders beside it and — for a buyer Stripe knows — the wire instructions they were given.
No capability token is served here. A buyer's order page and a quote page are each opened by a crypto-random token, and that token IS the credential — so the summaries below carry a ref and nothing that opens anything. A caller that needs a quote's buyer link asks GET /quotes/{ref}, where serving it is the point.
People and payment terms are the writes. people lists everyone who signs in as the buyer; POST /buyers/{ref}/people and DELETE /buyers/{ref}/people/{email} change it. terms is the payment terms the buyer's next invoices are issued under, null for each event's own settings; POST /buyers/{ref}/terms changes it. The rest of a buyer is read-only on every surface.
Requires orders.view
Path parameters
refstring required
Response fields
refstringemailstringnamestringorgstringphonestring- The buyer's phone number as it was typed, trimmed and cut to 64 characters. It isn't checked or reformatted. Empty when the buyer has none.
quote_countinteger- Quotes matching this buyer's id OR their email — an anonymous quote is still theirs.
order_countinteger- Orders matching their email, case-insensitively: buyers.email is lowercased on the way in and an order keeps the address as the buyer typed it, so a case-sensitive match once read a registered buyer as having no orders at all.
part_ofstring or null- The ref of the buyer this one is part of, like a department's company, or null when it is part of nothing. One level: a buyer others are part of is never part of another.
kindobject or null- The kind a buyer is, or null for none. A kind is a label for finding and filtering buyers; nothing else depends on it. The whole kind is
GET /buyer-kinds/{id}. kind.idintegerkind.namestringcreated_atstring (RFC 3339) or nulladmin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
stripe_customer_idstring- The Customer this buyer is on Stripe, empty until something mints one (the bank-transfer path does it lazily). An identifier, not a credential.
people[].emailstringpeople[].primaryboolean- The buyer's primary contact: the address it is listed under, the one invoices and Stripe use. It can't be removed; make someone else primary first (
POST /buyers/{ref}/primary). people[].rolestring- What this person can do when the buyer's quotes need approval: a requester sends a quote for approval, an approver can reject one. Where approval isn't needed the two can do the same things.
- one of
requesterapprover people[].sees_allboolean- Whether this person sees every one of the buyer's bookings and quotes when they sign in, or only the ones made with their own email address. Only the organizer can change it (
POST /buyers/{ref}/people/{email}/sees). domainsarray of string- The buyer's email domains, sorted, never null. Addresses at exactly these domains belong to the buyer before anybody signs in.
termsobject or null- The named payment terms a buyer has of its own, or null when it has none. A buyer without terms of its own follows the terms of the buyer it is part of (
inherited_termson the buyer), or else each event's own bank transfer settings. The whole set isGET /payment-terms/{id}. terms.idintegerterms.namestringinherited_termsobject- What a buyer without terms of its own follows: the terms of the buyer it is part of. Null when it has its own, is part of nothing, or that buyer has none either.
inherited_terms.idintegerinherited_terms.namestringincludes[].refstringincludes[].namestringpo_requiredboolean- This buyer's own approval setting: its quotes, and those of every buyer part of it, go to an approver before they can become orders. Always false on a buyer that is part of another, which follows that buyer's;
approvalsays what applies. Set it withPOST /buyers/{ref}/po-required. approvalobject- Approval as it applies to this buyer: required or not, and the buyer whose approvers answer, which is the buyer this one is part of, or else itself.
approval.requiredbooleanapproval.approverobjectapproval.approver.refstringapproval.approver.namestringblockobject- Whether the buyer is blocked from starting new orders and quotes, by its own block or the one on the buyer it is part of. Set it with
POST /buyers/{ref}/blockand lift it withDELETE /buyers/{ref}/block. block.blockedbooleanblock.messagestring- The words emailed to whoever at the buyer tries to order, as plain text, empty for the standard line.
block.blocked_atstring (RFC 3339) or null- When the block was set, null when the buyer isn't blocked.
block.byobject or null- The buyer whose block applies: this one, or the one it is part of. Null when the buyer isn't blocked.
block.by.refstringblock.by.namestringspendarray of object- What this buyer has actually PAID, per currency: order totals less whatever was refunded off them. A buyer can span currencies across events, so it is a list. A partial refund leaves the order paid and is netted; a full one flips its status and nets to zero either way.
spend[].centsintegerspend[].currencystringquotesarray of BuyerQuoteSummaryquotes[].refstringquotes[].event_slugstringquotes[].statusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
quotes[].amount_centsintegerquotes[].currencystringquotes[].expires_atstring (RFC 3339) or nullquotes[].created_atstring (RFC 3339) or nullquotes[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
ordersarray of BuyerOrderSummaryorders[].refstringorders[].event_slugstringorders[].statusstringorders[].amount_centsintegerorders[].currencystringorders[].payment_methodstringorders[].created_atstring (RFC 3339) or nullorders[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
bank_transferobject or null- The newest wire-instruction snapshot, null until this buyer has one. Read from the stored row — no Stripe call — because the virtual IBAN is per-Customer, so that row IS the account they pay into. The IBAN is here deliberately: it is the account's own receiving account, printed on the invoice the buyer was already sent, not a secret that opens anything.
bank_transfer.referencestringbank_transfer.ibanstringbank_transfer.bicstringbank_transfer.account_holderstringbank_transfer.bank_countrystringbank_transfer.amount_centsintegerbank_transfer.currencystringbank_transfer.due_atstring (RFC 3339) or nullbank_detailsobject or null- The bank details Stripe issued this buyer's current Stripe customer, kept on the buyer: the account the buyer pays every bank transfer into, and what its vendor form prints. Null until they are kept, either by
POST /buyers/{ref}/bank-detailsor by the buyer's first Stripe bank transfer. Details kept for a Stripe customer the buyer is no longer linked to are never served. Read from the stored row, with no Stripe call. bank_details.account_holderstringbank_details.ibanstringbank_details.bicstringbank_details.currencystringbillingobject- This customer's billing identity, as currently stored (never a computed fallback — a blank
namehere can still print asorgon an invoice). billing.namestring- The legal entity an invoice is addressed to. Blank falls back to
orgwhen an invoice is rendered. billing.line1stringbilling.line2stringbilling.citystringbilling.postcodestringbilling.vat_numberstring
Responses
200The buyer.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/buyers/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/buyers/H7K2PX", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"ref": "b7x9k2",
"email": "accounts@example.com",
"name": "Alex Smith",
"org": "Example Ltd",
"phone": "+1 202 555 0143",
"quote_count": 3,
"order_count": 5,
"part_of": "k3m8p1",
"kind": {
"id": 2,
"name": "Partners"
},
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito",
"anonymized_at": null,
"stripe_customer_id": "cus_1MEXAMPLE",
"people": [
{
"email": "accounts@example.com",
"primary": true,
"role": "approver",
"sees_all": true
}
],
"domains": [
"example.org"
],
"terms": {
"id": 3,
"name": "Members"
},
"inherited_terms": {
"id": 3,
"name": "Members"
},
"includes": [
{
"ref": "p2q7r4",
"name": "Example Ltd Design"
}
],
"po_required": false,
"approval": {
"required": true,
"approver": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
},
"block": {
"blocked": true,
"message": "Please settle your outstanding invoices with our accounts team before booking again.",
"blocked_at": "2026-10-21T11:30:00Z",
"by": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
},
"spend": [
{
"cents": 150000,
"currency": "eur"
}
],
"quotes": [
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "open",
"amount_cents": 14900,
"currency": "eur",
"order_ref": "H7K2PX",
"expires_at": "2026-10-14T09:00:00Z",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
],
"orders": [
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "paid",
"amount_cents": 14900,
"currency": "eur",
"payment_method": "example",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
],
"bank_transfer": {
"reference": "RF97TESTREF",
"iban": "example",
"bic": "example",
"account_holder": "Ada Lovelace",
"bank_country": "example",
"amount_cents": 14900,
"currency": "eur",
"status": "awaiting",
"due_at": "2026-10-14T09:00:00Z"
},
"bank_details": {
"account_holder": "Stripe Payments Europe",
"iban": "IE29AIBK93115212345678",
"bic": "AIBKIE2D",
"bank_country": "IE",
"currency": "eur"
},
"billing": {
"name": "St Brigid's NS Board of Management",
"line1": "12 Main Street",
"line2": "",
"city": "Springfield",
"postcode": "D02 AF30",
"subdivision": "Co. Dublin",
"country": "IE",
"vat_number": "IE1234567T",
"buyer_reference": "",
"po_reference": ""
}
}
Change a buyer's name, organization, phone number or billing identity
PATCH /buyers/{ref}
Sets the buyer's own name, organization, phone number and billing identity, any of them. An empty phone clears it; billing fields follow the same per-field set/clear/leave-alone rule. The primary contact's email is not changed here. An already-issued bank-transfer invoice keeps the name, organization and billing address it was issued with.
Requires orders.manage
Path parameters
refstring required
Request body
namestringorgstringphonestringbillingobject- Any of the billing fields, patched one at a time: a field absent from
billingleaves its column alone, present with a value sets it, present as""clears it.countrymust be an ISO 3166-1 alpha-2 code or blank. billing.namestring- The legal entity an invoice is addressed to. Blank falls back to
orgwhen an invoice is rendered. billing.line1stringbilling.line2stringbilling.citystringbilling.postcodestringbilling.vat_numberstring
Response fields
refstringemailstringnamestringorgstringphonestring- The buyer's phone number as it was typed, trimmed and cut to 64 characters. It isn't checked or reformatted. Empty when the buyer has none.
quote_countinteger- Quotes matching this buyer's id OR their email — an anonymous quote is still theirs.
order_countinteger- Orders matching their email, case-insensitively: buyers.email is lowercased on the way in and an order keeps the address as the buyer typed it, so a case-sensitive match once read a registered buyer as having no orders at all.
part_ofstring or null- The ref of the buyer this one is part of, like a department's company, or null when it is part of nothing. One level: a buyer others are part of is never part of another.
kindobject or null- The kind a buyer is, or null for none. A kind is a label for finding and filtering buyers; nothing else depends on it. The whole kind is
GET /buyer-kinds/{id}. kind.idintegerkind.namestringcreated_atstring (RFC 3339) or nulladmin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
stripe_customer_idstring- The Customer this buyer is on Stripe, empty until something mints one (the bank-transfer path does it lazily). An identifier, not a credential.
people[].emailstringpeople[].primaryboolean- The buyer's primary contact: the address it is listed under, the one invoices and Stripe use. It can't be removed; make someone else primary first (
POST /buyers/{ref}/primary). people[].rolestring- What this person can do when the buyer's quotes need approval: a requester sends a quote for approval, an approver can reject one. Where approval isn't needed the two can do the same things.
- one of
requesterapprover people[].sees_allboolean- Whether this person sees every one of the buyer's bookings and quotes when they sign in, or only the ones made with their own email address. Only the organizer can change it (
POST /buyers/{ref}/people/{email}/sees). domainsarray of string- The buyer's email domains, sorted, never null. Addresses at exactly these domains belong to the buyer before anybody signs in.
termsobject or null- The named payment terms a buyer has of its own, or null when it has none. A buyer without terms of its own follows the terms of the buyer it is part of (
inherited_termson the buyer), or else each event's own bank transfer settings. The whole set isGET /payment-terms/{id}. terms.idintegerterms.namestringinherited_termsobject- What a buyer without terms of its own follows: the terms of the buyer it is part of. Null when it has its own, is part of nothing, or that buyer has none either.
inherited_terms.idintegerinherited_terms.namestringincludes[].refstringincludes[].namestringpo_requiredboolean- This buyer's own approval setting: its quotes, and those of every buyer part of it, go to an approver before they can become orders. Always false on a buyer that is part of another, which follows that buyer's;
approvalsays what applies. Set it withPOST /buyers/{ref}/po-required. approvalobject- Approval as it applies to this buyer: required or not, and the buyer whose approvers answer, which is the buyer this one is part of, or else itself.
approval.requiredbooleanapproval.approverobjectapproval.approver.refstringapproval.approver.namestringblockobject- Whether the buyer is blocked from starting new orders and quotes, by its own block or the one on the buyer it is part of. Set it with
POST /buyers/{ref}/blockand lift it withDELETE /buyers/{ref}/block. block.blockedbooleanblock.messagestring- The words emailed to whoever at the buyer tries to order, as plain text, empty for the standard line.
block.blocked_atstring (RFC 3339) or null- When the block was set, null when the buyer isn't blocked.
block.byobject or null- The buyer whose block applies: this one, or the one it is part of. Null when the buyer isn't blocked.
block.by.refstringblock.by.namestringspendarray of object- What this buyer has actually PAID, per currency: order totals less whatever was refunded off them. A buyer can span currencies across events, so it is a list. A partial refund leaves the order paid and is netted; a full one flips its status and nets to zero either way.
spend[].centsintegerspend[].currencystringquotesarray of BuyerQuoteSummaryquotes[].refstringquotes[].event_slugstringquotes[].statusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
quotes[].amount_centsintegerquotes[].currencystringquotes[].expires_atstring (RFC 3339) or nullquotes[].created_atstring (RFC 3339) or nullquotes[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
ordersarray of BuyerOrderSummaryorders[].refstringorders[].event_slugstringorders[].statusstringorders[].amount_centsintegerorders[].currencystringorders[].payment_methodstringorders[].created_atstring (RFC 3339) or nullorders[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
bank_transferobject or null- The newest wire-instruction snapshot, null until this buyer has one. Read from the stored row — no Stripe call — because the virtual IBAN is per-Customer, so that row IS the account they pay into. The IBAN is here deliberately: it is the account's own receiving account, printed on the invoice the buyer was already sent, not a secret that opens anything.
bank_transfer.referencestringbank_transfer.ibanstringbank_transfer.bicstringbank_transfer.account_holderstringbank_transfer.bank_countrystringbank_transfer.amount_centsintegerbank_transfer.currencystringbank_transfer.due_atstring (RFC 3339) or nullbank_detailsobject or null- The bank details Stripe issued this buyer's current Stripe customer, kept on the buyer: the account the buyer pays every bank transfer into, and what its vendor form prints. Null until they are kept, either by
POST /buyers/{ref}/bank-detailsor by the buyer's first Stripe bank transfer. Details kept for a Stripe customer the buyer is no longer linked to are never served. Read from the stored row, with no Stripe call. bank_details.account_holderstringbank_details.ibanstringbank_details.bicstringbank_details.currencystringbillingobject- This customer's billing identity, as currently stored (never a computed fallback — a blank
namehere can still print asorgon an invoice). billing.namestring- The legal entity an invoice is addressed to. Blank falls back to
orgwhen an invoice is rendered. billing.line1stringbilling.line2stringbilling.citystringbilling.postcodestringbilling.vat_numberstring
Responses
200The buyer as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body carries none ofname,org,phoneandbilling, or a key that isn't one of them (invalid_body), orbilling.countryisn't a valid ISO 3166-1 alpha-2 code (invalid_country).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Alex Smith"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Alex Smith"
}),
});
const data = await res.json();
{
"ref": "b7x9k2",
"email": "accounts@example.com",
"name": "Alex Smith",
"org": "Example Ltd",
"phone": "+1 202 555 0143",
"quote_count": 3,
"order_count": 5,
"part_of": "k3m8p1",
"kind": {
"id": 2,
"name": "Partners"
},
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito",
"anonymized_at": null,
"stripe_customer_id": "cus_1MEXAMPLE",
"people": [
{
"email": "accounts@example.com",
"primary": true,
"role": "approver",
"sees_all": true
}
],
"domains": [
"example.org"
],
"terms": {
"id": 3,
"name": "Members"
},
"inherited_terms": {
"id": 3,
"name": "Members"
},
"includes": [
{
"ref": "p2q7r4",
"name": "Example Ltd Design"
}
],
"po_required": false,
"approval": {
"required": true,
"approver": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
},
"block": {
"blocked": true,
"message": "Please settle your outstanding invoices with our accounts team before booking again.",
"blocked_at": "2026-10-21T11:30:00Z",
"by": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
},
"spend": [
{
"cents": 150000,
"currency": "eur"
}
],
"quotes": [
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "open",
"amount_cents": 14900,
"currency": "eur",
"order_ref": "H7K2PX",
"expires_at": "2026-10-14T09:00:00Z",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
],
"orders": [
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "paid",
"amount_cents": 14900,
"currency": "eur",
"payment_method": "example",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
],
"bank_transfer": {
"reference": "RF97TESTREF",
"iban": "example",
"bic": "example",
"account_holder": "Ada Lovelace",
"bank_country": "example",
"amount_cents": 14900,
"currency": "eur",
"status": "awaiting",
"due_at": "2026-10-14T09:00:00Z"
},
"bank_details": {
"account_holder": "Stripe Payments Europe",
"iban": "IE29AIBK93115212345678",
"bic": "AIBKIE2D",
"bank_country": "IE",
"currency": "eur"
},
"billing": {
"name": "St Brigid's NS Board of Management",
"line1": "12 Main Street",
"line2": "",
"city": "Springfield",
"postcode": "D02 AF30",
"subdivision": "Co. Dublin",
"country": "IE",
"vat_number": "IE1234567T",
"buyer_reference": "",
"po_reference": ""
}
}
Anonymize a customer
POST /buyers/{ref}/anonymize
Remove a customer: its name, email address, organization, phone number and billing address, its people and email domains, its bank details and letters, and the buyer details on every order and quote it placed. The attendees on those orders are not changed: they are often other people. An agent limited to some events cannot call this. It happens at once and cannot be undone, the API's copy of the admin's Anonymize confirm step, which runs the same code. What stays: the record itself with its reference, status and place in every count, what was bought and paid and when, the payment reference, and every invoice, receipt and credit note already issued, exactly as issued. Copies of the person in sent emails, the record's history, its webhook deliveries, workflow runs and the API request log are cleared too. What Tito cannot reach is named in outside_tito: the payment provider's own record, connected services the person was sent to, and whatever your webhooks already delivered. Every call writes a line to the data-protection log (GET /data-protection-log) that names the record's reference and never the person. The link tags its orders arrived with go too, ad click ids and your own tags included, except utm_* campaign tags, which stay so the order still counts toward the link that sold it. Send no request body; any body is ignored. An order still waiting for a bank transfer is canceled first, so a payment that arrives later cannot fulfil it; one whose tickets were already issued is left as it is.
Requires erasure
Path parameters
refstring required- The customer's ref.
Response fields
refstring- The record's reference: a ticket, order, customer, waitlist entry or ticket request ref, or the invitation's id.
anonymized_atstring (RFC 3339)removedobjectremoved.emailsinteger- Sent emails whose address and words were removed. The record that each was sent stays.
removed.link_tagsinteger- Orders whose link tags were removed: every tag that isn't a
utm_*campaign tag, ad click ids included. removed.interestboolean- Their email address was taken off the mailing list, where they signed up to hear when tickets go on sale.
removed.cardboolean- A card saved with a ticket request this withdrew, which Stripe was asked to delete.
keptobjectkept.other_recordsinteger- Other orders, tickets and invitations with the same email address, which are not changed.
kept.customerstring or null- The customer record an anonymized order is linked to, which is not changed.
kept.campaign_tagsinteger- Orders whose
utm_*campaign tags were kept, so they still count toward the link that sold them. kept.orderstring or null- The order a waitlist place was taken with, or that a ticket request's invitation was claimed with, which is not changed.
kept.invitationinteger or null- The invitation a ticket request was approved into, which is not changed.
outside_titoobjectoutside_tito.payment_providerstring or null- The provider that keeps its own record of the payment, with the buyer's name and email address.
- one of
StripePayPal<nil> outside_tito.servicesarray of string- Connected services the person's details were sent to, which keep their own copy.
outside_tito.cardboolean- A card saved with a ticket request that was already decided, which Stripe keeps.
Responses
200The customer is anonymized.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The person on this record has already been anonymized (already_anonymized). Nothing changes.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/anonymize \
-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/buyers/H7K2PX/anonymize", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"anonymized": true,
"kind": "ticket",
"ref": "H7K2PX",
"anonymized_at": "2026-10-14T09:00:00Z",
"removed": {
"tickets": 1,
"orders": 1,
"quotes": 1,
"answers": 1,
"files": 1,
"emails": 1,
"contact": true,
"waitlist": true,
"ticket_request": true,
"link_tags": 1,
"interest": true,
"card": true
},
"kept": {
"documents": 1,
"other_records": 1,
"customer": "example",
"campaign_tags": 1,
"order": "H7K2PX",
"ticket": "T9QW4M",
"invitation": 1
},
"outside_tito": {
"payment_provider": "Stripe",
"services": [
"example"
],
"webhooks": true,
"card": true
}
}
Get a buyer's bank details from Stripe
POST /buyers/{ref}/bank-details
Ask Stripe for the bank details it issues this buyer's Stripe customer, the account the buyer pays bank transfers into, and keep them on the buyer. They then show on the buyer's page, print on its vendor form, and appear as bank_details on the buyer. A buyer with no Stripe customer yet is added to the account's Stripe first. No body.
Calling it for a buyer whose bank details are already kept for its current Stripe customer changes nothing and doesn't call Stripe. Nothing is kept if Stripe can't be reached or refuses. Asking Stripe can create the account details the first time, which is why this is a POST and nothing that reads a buyer ever calls it.
Stripe allows each account up to 1,000 of these account details. Once Tito keeps bank details for 900 buyers on an account, it stops asking Stripe for new ones (409 bank_details_limit), so the rest stay free for buyers paying by bank transfer at checkout. Bank details a buyer already has can still be asked for again.
Requires payment.manage
Path parameters
refstring required
Response fields
refstringemailstringnamestringorgstringphonestring- The buyer's phone number as it was typed, trimmed and cut to 64 characters. It isn't checked or reformatted. Empty when the buyer has none.
quote_countinteger- Quotes matching this buyer's id OR their email — an anonymous quote is still theirs.
order_countinteger- Orders matching their email, case-insensitively: buyers.email is lowercased on the way in and an order keeps the address as the buyer typed it, so a case-sensitive match once read a registered buyer as having no orders at all.
part_ofstring or null- The ref of the buyer this one is part of, like a department's company, or null when it is part of nothing. One level: a buyer others are part of is never part of another.
kindobject or null- The kind a buyer is, or null for none. A kind is a label for finding and filtering buyers; nothing else depends on it. The whole kind is
GET /buyer-kinds/{id}. kind.idintegerkind.namestringcreated_atstring (RFC 3339) or nulladmin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
stripe_customer_idstring- The Customer this buyer is on Stripe, empty until something mints one (the bank-transfer path does it lazily). An identifier, not a credential.
people[].emailstringpeople[].primaryboolean- The buyer's primary contact: the address it is listed under, the one invoices and Stripe use. It can't be removed; make someone else primary first (
POST /buyers/{ref}/primary). people[].rolestring- What this person can do when the buyer's quotes need approval: a requester sends a quote for approval, an approver can reject one. Where approval isn't needed the two can do the same things.
- one of
requesterapprover people[].sees_allboolean- Whether this person sees every one of the buyer's bookings and quotes when they sign in, or only the ones made with their own email address. Only the organizer can change it (
POST /buyers/{ref}/people/{email}/sees). domainsarray of string- The buyer's email domains, sorted, never null. Addresses at exactly these domains belong to the buyer before anybody signs in.
termsobject or null- The named payment terms a buyer has of its own, or null when it has none. A buyer without terms of its own follows the terms of the buyer it is part of (
inherited_termson the buyer), or else each event's own bank transfer settings. The whole set isGET /payment-terms/{id}. terms.idintegerterms.namestringinherited_termsobject- What a buyer without terms of its own follows: the terms of the buyer it is part of. Null when it has its own, is part of nothing, or that buyer has none either.
inherited_terms.idintegerinherited_terms.namestringincludes[].refstringincludes[].namestringpo_requiredboolean- This buyer's own approval setting: its quotes, and those of every buyer part of it, go to an approver before they can become orders. Always false on a buyer that is part of another, which follows that buyer's;
approvalsays what applies. Set it withPOST /buyers/{ref}/po-required. approvalobject- Approval as it applies to this buyer: required or not, and the buyer whose approvers answer, which is the buyer this one is part of, or else itself.
approval.requiredbooleanapproval.approverobjectapproval.approver.refstringapproval.approver.namestringblockobject- Whether the buyer is blocked from starting new orders and quotes, by its own block or the one on the buyer it is part of. Set it with
POST /buyers/{ref}/blockand lift it withDELETE /buyers/{ref}/block. block.blockedbooleanblock.messagestring- The words emailed to whoever at the buyer tries to order, as plain text, empty for the standard line.
block.blocked_atstring (RFC 3339) or null- When the block was set, null when the buyer isn't blocked.
block.byobject or null- The buyer whose block applies: this one, or the one it is part of. Null when the buyer isn't blocked.
block.by.refstringblock.by.namestringspendarray of object- What this buyer has actually PAID, per currency: order totals less whatever was refunded off them. A buyer can span currencies across events, so it is a list. A partial refund leaves the order paid and is netted; a full one flips its status and nets to zero either way.
spend[].centsintegerspend[].currencystringquotesarray of BuyerQuoteSummaryquotes[].refstringquotes[].event_slugstringquotes[].statusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
quotes[].amount_centsintegerquotes[].currencystringquotes[].expires_atstring (RFC 3339) or nullquotes[].created_atstring (RFC 3339) or nullquotes[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
ordersarray of BuyerOrderSummaryorders[].refstringorders[].event_slugstringorders[].statusstringorders[].amount_centsintegerorders[].currencystringorders[].payment_methodstringorders[].created_atstring (RFC 3339) or nullorders[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
bank_transferobject or null- The newest wire-instruction snapshot, null until this buyer has one. Read from the stored row — no Stripe call — because the virtual IBAN is per-Customer, so that row IS the account they pay into. The IBAN is here deliberately: it is the account's own receiving account, printed on the invoice the buyer was already sent, not a secret that opens anything.
bank_transfer.referencestringbank_transfer.ibanstringbank_transfer.bicstringbank_transfer.account_holderstringbank_transfer.bank_countrystringbank_transfer.amount_centsintegerbank_transfer.currencystringbank_transfer.due_atstring (RFC 3339) or nullbank_detailsobject or null- The bank details Stripe issued this buyer's current Stripe customer, kept on the buyer: the account the buyer pays every bank transfer into, and what its vendor form prints. Null until they are kept, either by
POST /buyers/{ref}/bank-detailsor by the buyer's first Stripe bank transfer. Details kept for a Stripe customer the buyer is no longer linked to are never served. Read from the stored row, with no Stripe call. bank_details.account_holderstringbank_details.ibanstringbank_details.bicstringbank_details.currencystringbillingobject- This customer's billing identity, as currently stored (never a computed fallback — a blank
namehere can still print asorgon an invoice). billing.namestring- The legal entity an invoice is addressed to. Blank falls back to
orgwhen an invoice is rendered. billing.line1stringbilling.line2stringbilling.citystringbilling.postcodestringbilling.vat_numberstring
Responses
200The buyer as it now stands, with itsbank_details.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The account has no Stripe connection (not_connected), no event takes bank transfers on the Stripe rail so Stripe issues its buyers no bank details (manual_rail), the account's Stripe has no customer with the buyer'sstripe_customer_id(stripe_customer_missing), the buyer's Stripe customer changed while the request was in flight (stripe_customer_moved), or the account already keeps bank details for 900 buyers (bank_details_limit). Nothing is kept.500Internal error.502Stripe couldn't be asked, and nothing was kept (stripe_unavailable). Safe to retry. Stripe refusing this account's request answersstripe_refusedinstead; check the account's Stripe connection before retrying.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/bank-details \
-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/buyers/H7K2PX/bank-details", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"ref": "b7x9k2",
"email": "accounts@example.com",
"name": "Alex Smith",
"org": "Example Ltd",
"phone": "+1 202 555 0143",
"quote_count": 3,
"order_count": 5,
"part_of": "k3m8p1",
"kind": {
"id": 2,
"name": "Partners"
},
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito",
"anonymized_at": null,
"stripe_customer_id": "cus_1MEXAMPLE",
"people": [
{
"email": "accounts@example.com",
"primary": true,
"role": "approver",
"sees_all": true
}
],
"domains": [
"example.org"
],
"terms": {
"id": 3,
"name": "Members"
},
"inherited_terms": {
"id": 3,
"name": "Members"
},
"includes": [
{
"ref": "p2q7r4",
"name": "Example Ltd Design"
}
],
"po_required": false,
"approval": {
"required": true,
"approver": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
},
"block": {
"blocked": true,
"message": "Please settle your outstanding invoices with our accounts team before booking again.",
"blocked_at": "2026-10-21T11:30:00Z",
"by": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
},
"spend": [
{
"cents": 150000,
"currency": "eur"
}
],
"quotes": [
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "open",
"amount_cents": 14900,
"currency": "eur",
"order_ref": "H7K2PX",
"expires_at": "2026-10-14T09:00:00Z",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
],
"orders": [
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "paid",
"amount_cents": 14900,
"currency": "eur",
"payment_method": "example",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
],
"bank_transfer": {
"reference": "RF97TESTREF",
"iban": "example",
"bic": "example",
"account_holder": "Ada Lovelace",
"bank_country": "example",
"amount_cents": 14900,
"currency": "eur",
"status": "awaiting",
"due_at": "2026-10-14T09:00:00Z"
},
"bank_details": {
"account_holder": "Stripe Payments Europe",
"iban": "IE29AIBK93115212345678",
"bic": "AIBKIE2D",
"bank_country": "IE",
"currency": "eur"
},
"billing": {
"name": "St Brigid's NS Board of Management",
"line1": "12 Main Street",
"line2": "",
"city": "Springfield",
"postcode": "D02 AF30",
"subdivision": "Co. Dublin",
"country": "IE",
"vat_number": "IE1234567T",
"buyer_reference": "",
"po_reference": ""
}
}
Unblock a buyer
DELETE /buyers/{ref}/block
Removes the buyer's own block, so its people can place orders and generate quotes again. A block on the buyer it is part of still applies, and the answer says so.
Requires orders.manage
Path parameters
refstring required
Response fields
blockobject- Whether the buyer is blocked from starting new orders and quotes, by its own block or the one on the buyer it is part of. Set it with
POST /buyers/{ref}/blockand lift it withDELETE /buyers/{ref}/block. block.blockedbooleanblock.messagestring- The words emailed to whoever at the buyer tries to order, as plain text, empty for the standard line.
block.blocked_atstring (RFC 3339) or null- When the block was set, null when the buyer isn't blocked.
block.byobject or null- The buyer whose block applies: this one, or the one it is part of. Null when the buyer isn't blocked.
block.by.refstringblock.by.namestring
Responses
200The block as it now applies to the buyer.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/buyers/H7K2PX/block \
-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/buyers/H7K2PX/block", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"block": {
"blocked": true,
"message": "Please settle your outstanding invoices with our accounts team before booking again.",
"blocked_at": "2026-10-21T11:30:00Z",
"by": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
}
}
Block a buyer from new orders and quotes
POST /buyers/{ref}/block
While a buyer is blocked, its people can't place a new order or generate a quote, whichever way they pay, free orders included. They can still pay for orders already placed, and you can still make orders for the buyer yourself. A buyer that is part of this one is blocked too. Someone is recognized as the buyer's when they're signed in as it, or check out with an address on its people or at one of its email domains. message is emailed to whoever tries, as plain text (formatting is set on the customer's page); leave it out or empty for the standard line. Posting again to a blocked buyer changes the message and keeps blocked_at.
Requires orders.manage
Path parameters
refstring required
Request body
messagestring
Response fields
blockobject- Whether the buyer is blocked from starting new orders and quotes, by its own block or the one on the buyer it is part of. Set it with
POST /buyers/{ref}/blockand lift it withDELETE /buyers/{ref}/block. block.blockedbooleanblock.messagestring- The words emailed to whoever at the buyer tries to order, as plain text, empty for the standard line.
block.blocked_atstring (RFC 3339) or null- When the block was set, null when the buyer isn't blocked.
block.byobject or null- The buyer whose block applies: this one, or the one it is part of. Null when the buyer isn't blocked.
block.by.refstringblock.by.namestring
Responses
200The block as it now applies to the buyer.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The buyer has been anonymized (anonymized).422The body isn't an object with an optionalmessage(invalid_body), or the message is over 1000 characters (message_too_long).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/block \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"message":"Please settle your outstanding invoices with our accounts team before booking again."}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/block", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"message": "Please settle your outstanding invoices with our accounts team before booking again."
}),
});
const data = await res.json();
{
"block": {
"blocked": true,
"message": "Please settle your outstanding invoices with our accounts team before booking again.",
"blocked_at": "2026-10-21T11:30:00Z",
"by": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
}
}
Add an email domain to a buyer
POST /buyers/{ref}/domains
Put an email domain on a buyer: the part after the @, like example.org. Addresses at exactly that domain belong to the buyer before anybody signs in, so a signed-out checkout with one follows the buyer's approval rules. Only the exact domain matches, never a subdomain, so mail.example.org is a different one. The domain is stored lowercased, with any leading @ taken off. A domain can be on more than one buyer, like a board whose schools share its domain. Answers with the buyer's domains as they now stand, sorted. Something that isn't a domain answers 422 invalid_domain; a domain anyone can get an address at, like gmail.com or yahoo.ie, answers 422 public_provider; one already on this buyer answers 409 already_on; and a buyer that already has 20 answers 422 too_many.
Requires orders.manage
Path parameters
refstring required
Request body
Response fields
domainsarray of string
Responses
201The domain was added.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409That domain is already one of this buyer's (already_on).422The body isn't{"domain":"..."}(invalid_body), the domain isn't one (invalid_domain), anyone can get an address at it (public_provider), or the buyer already has 20 (too_many).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/domains \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"domain":"example.org"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/domains", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"domain": "example.org"
}),
});
const data = await res.json();
{
"domains": [
"example.org"
]
}
Remove an email domain from a buyer
DELETE /buyers/{ref}/domains/{domain}
Take an email domain off a buyer. Addresses at it stop belonging to the buyer from the next checkout; quotes and orders already placed for the buyer stay with it. A domain that isn't one of the buyer's answers 404 not_on.
Requires orders.manage
Path parameters
refstring requireddomainstring required
Response fields
domainsarray of string
Responses
200The domain was removed.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such buyer, or the domain isn't one of its (not_on).500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/domains/{domain} \
-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/buyers/H7K2PX/domains/{domain}", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"domains": [
"example.org"
]
}
Give a buyer a kind
POST /buyers/{ref}/kind
Choose which of the account's kinds this buyer is. 0 leaves the buyer with no kind. A kind is a label for finding and filtering buyers; nothing else depends on it. A kind_id that names no kind answers 422 unknown_kind.
Requires orders.manage
Path parameters
refstring required
Request body
Response fields
kindobject or null- The kind a buyer is, or null for none. A kind is a label for finding and filtering buyers; nothing else depends on it. The whole kind is
GET /buyer-kinds/{id}. kind.idintegerkind.namestring
Responses
200The buyer's kind as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body isn't{"kind_id": n}(invalid_body), or no kind has that id (unknown_kind).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/kind \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"kind_id":2}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/kind", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"kind_id": 2
}),
});
const data = await res.json();
{
"kind": {
"id": 2,
"name": "Partners"
}
}
Make a buyer part of another
POST /buyers/{ref}/part-of
Choose which buyer this one is part of, like a department under its company, or null for part of nothing. A buyer without payment terms of its own follows the terms of the buyer it is part of. It is one level: the chosen buyer must not be part of another itself (409 part_of_another), and a buyer that others are part of can't be part of another (409 has_buyers).
Requires orders.manage
Path parameters
refstring required
Request body
part_ofstring or null required- The ref of the buyer this one is part of, or null for part of nothing.
Response fields
part_ofstring or null
Responses
200What the buyer is now part of.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The chosen buyer is part of another (part_of_another), or other buyers are part of this one (has_buyers).422The body isn't{"part_of": ...}(invalid_body),part_ofnames no buyer (unknown_buyer), or names this buyer (part_of_itself).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/part-of \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"part_of":"k3m8p1"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/part-of", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"part_of": "k3m8p1"
}),
});
const data = await res.json();
{
"part_of": "example"
}
Add a person to a buyer
POST /buyers/{ref}/people
Put an email address on a buyer, so that person can sign in as the buyer with their own address and see its quotes, orders and invoices. They can sign in straight away; nobody is emailed. Answers with the buyer's people as they now stand. An address that isn't one answers 422 invalid_email; one already on this buyer answers 409 already_on. An address may be on more than one buyer; when they sign in, they choose which. Give a role to choose what they can do when quotes need approval; a role that isn't one answers 422 invalid_role. To add somebody from the address book, send contact with their contact ref in place of email. That needs a key carrying contacts.view as well, or it answers 403 missing_capability, and a ref that is no contact, or Contacts switched off, answers 422 unknown_contact. Send one of email and contact, never both.
Requires orders.manage
Path parameters
refstring required
Request body
contactstring- A contact's ref, to add that contact's address. Send this or
email. Needscontacts.viewas well asorders.manage. rolestring- Optional. Without it the person is a requester where the buyer's quotes need approval, and an approver everywhere else.
- one of
requesterapprover
Response fields
peoplearray of BuyerPersonpeople[].emailstringpeople[].primaryboolean- The buyer's primary contact: the address it is listed under, the one invoices and Stripe use. It can't be removed; make someone else primary first (
POST /buyers/{ref}/primary). people[].rolestring- What this person can do when the buyer's quotes need approval: a requester sends a quote for approval, an approver can reject one. Where approval isn't needed the two can do the same things.
- one of
requesterapprover people[].sees_allboolean- Whether this person sees every one of the buyer's bookings and quotes when they sign in, or only the ones made with their own email address. Only the organizer can change it (
POST /buyers/{ref}/people/{email}/sees).
Responses
201The person was added.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409That address is already one of this buyer's people (already_on).422The body isn't{"email":"..."}or{"contact":"..."}(invalid_body), the address isn't one (invalid_email), the contact ref is no contact (unknown_contact), or the role isn'trequesterorapprover(invalid_role).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/people \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/people", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"email": "ada@example.com"
}),
});
const data = await res.json();
{
"people": [
{
"email": "accounts@example.com",
"primary": true,
"role": "approver",
"sees_all": true
}
]
}
Remove a person from a buyer
DELETE /buyers/{ref}/people/{email}
Take an email address off a buyer. The person can no longer sign in as the buyer and is signed out at once; quotes and orders they already placed stay with the buyer. The primary contact, the address the buyer registered with, can't be removed and answers 409 primary_contact. An address that isn't one of the buyer's people answers 404 not_on. URL-encode the address in the path.
Requires orders.manage
Path parameters
refstring requiredemailstring required
Response fields
peoplearray of BuyerPersonpeople[].emailstringpeople[].primaryboolean- The buyer's primary contact: the address it is listed under, the one invoices and Stripe use. It can't be removed; make someone else primary first (
POST /buyers/{ref}/primary). people[].rolestring- What this person can do when the buyer's quotes need approval: a requester sends a quote for approval, an approver can reject one. Where approval isn't needed the two can do the same things.
- one of
requesterapprover people[].sees_allboolean- Whether this person sees every one of the buyer's bookings and quotes when they sign in, or only the ones made with their own email address. Only the organizer can change it (
POST /buyers/{ref}/people/{email}/sees).
Responses
200The person was removed.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such buyer, or the address isn't one of its people (not_on).409The primary contact can't be removed (primary_contact).500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/people/{email} \
-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/buyers/H7K2PX/people/{email}", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"people": [
{
"email": "accounts@example.com",
"primary": true,
"role": "approver",
"sees_all": true
}
]
}
Change a person's role on a buyer
POST /buyers/{ref}/people/{email}/role
Make one of a buyer's people a requester or an approver. Where the buyer's quotes need approval, a requester sends a quote for approval and an approver can reject one. Answers with the buyer's people as they now stand.
Requires orders.manage
Path parameters
refstring requiredemailstring required
Request body
Response fields
peoplearray of BuyerPersonpeople[].emailstringpeople[].primaryboolean- The buyer's primary contact: the address it is listed under, the one invoices and Stripe use. It can't be removed; make someone else primary first (
POST /buyers/{ref}/primary). people[].rolestring- What this person can do when the buyer's quotes need approval: a requester sends a quote for approval, an approver can reject one. Where approval isn't needed the two can do the same things.
- one of
requesterapprover people[].sees_allboolean- Whether this person sees every one of the buyer's bookings and quotes when they sign in, or only the ones made with their own email address. Only the organizer can change it (
POST /buyers/{ref}/people/{email}/sees).
Responses
200The buyer's people.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such buyer (not_found), or the address isn't one of its people (not_on).422The body isn't{"role": ...}(invalid_body), or the role isn'trequesterorapprover(invalid_role).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/people/{email}/role \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"role":"requester"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/people/{email}/role", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"role": "requester"
}),
});
const data = await res.json();
{
"people": [
{
"email": "accounts@example.com",
"primary": true,
"role": "approver",
"sees_all": true
}
]
}
Change what a person sees on a buyer
POST /buyers/{ref}/people/{email}/sees
Choose whether one of a buyer's people sees every one of the buyer's bookings and quotes when they sign in (true), or only the ones made with their own email address (false, the default). Answers with the buyer's people as they now stand.
Requires orders.manage
Path parameters
refstring requiredemailstring required
Request body
sees_allboolean required
Response fields
peoplearray of BuyerPersonpeople[].emailstringpeople[].primaryboolean- The buyer's primary contact: the address it is listed under, the one invoices and Stripe use. It can't be removed; make someone else primary first (
POST /buyers/{ref}/primary). people[].rolestring- What this person can do when the buyer's quotes need approval: a requester sends a quote for approval, an approver can reject one. Where approval isn't needed the two can do the same things.
- one of
requesterapprover people[].sees_allboolean- Whether this person sees every one of the buyer's bookings and quotes when they sign in, or only the ones made with their own email address. Only the organizer can change it (
POST /buyers/{ref}/people/{email}/sees).
Responses
200The buyer's people.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such buyer (not_found), or the address isn't one of its people (not_on).422The body isn't{"sees_all": true}or{"sees_all": false}(invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/people/{email}/sees \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"sees_all":true}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/people/{email}/sees", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"sees_all": true
}),
});
const data = await res.json();
{
"people": [
{
"email": "accounts@example.com",
"primary": true,
"role": "approver",
"sees_all": true
}
]
}
Choose whether a buyer's quotes need approval
POST /buyers/{ref}/po-required
With po_required on, a quote made for this buyer, or for any buyer part of it, goes to this buyer's approvers before it can become an order. It is set on the buyer that approves: a buyer that is part of another follows that buyer's setting and can't have its own (409 part_of).
Requires orders.manage
Path parameters
refstring required
Request body
po_requiredboolean required
Response fields
po_requiredbooleanapprovalobject- Approval as it applies to this buyer: required or not, and the buyer whose approvers answer, which is the buyer this one is part of, or else itself.
approval.requiredbooleanapproval.approverobjectapproval.approver.refstringapproval.approver.namestring
Responses
200The buyer's setting, and approval as it now applies.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The buyer is part of another, whose setting applies to it (part_of).422The body isn't{"po_required": true}or{"po_required": false}(invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/po-required \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"po_required":true}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/po-required", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"po_required": true
}),
});
const data = await res.json();
{
"po_required": true,
"approval": {
"required": true,
"approver": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
}
}
Make a person a buyer's primary contact
POST /buyers/{ref}/primary
Make one of a buyer's people its primary contact: the address the buyer is listed under, the one its Stripe Customer carries, and the one a buyer import matches it by. The previous primary contact stays on as one of the buyer's people, and nobody is signed out. Making the current primary contact primary again changes nothing. Answers with the buyer's people as they now stand. An address that isn't one of the buyer's people answers 404 not_on; one that is already another buyer's primary contact answers 409 primary_taken, because an address can be the primary contact of one buyer only. A change that lands while this request is in flight answers 409 primary_moved. The previous primary contact's orders and quotes from before the buyer had people stay with the buyer. The buyer's name is the primary contact's own, so it is cleared; set the new contact's with PATCH /buyers/{ref}.
Requires orders.manage
Path parameters
refstring required
Request body
emailstring required
Response fields
peoplearray of BuyerPersonpeople[].emailstringpeople[].primaryboolean- The buyer's primary contact: the address it is listed under, the one invoices and Stripe use. It can't be removed; make someone else primary first (
POST /buyers/{ref}/primary). people[].rolestring- What this person can do when the buyer's quotes need approval: a requester sends a quote for approval, an approver can reject one. Where approval isn't needed the two can do the same things.
- one of
requesterapprover people[].sees_allboolean- Whether this person sees every one of the buyer's bookings and quotes when they sign in, or only the ones made with their own email address. Only the organizer can change it (
POST /buyers/{ref}/people/{email}/sees).
Responses
200The person is now the primary contact.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such buyer, or the address isn't one of its people (not_on).409The address is already another buyer's primary contact (primary_taken), or this buyer's primary contact changed while the request was in flight (primary_moved).422The body isn't{"email":"..."}(invalid_body), or the address is blank (invalid_email).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/primary \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/primary", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"email": "ada@example.com"
}),
});
const data = await res.json();
{
"people": [
{
"email": "accounts@example.com",
"primary": true,
"role": "approver",
"sees_all": true
}
]
}
Link a buyer to a Stripe customer
POST /buyers/{ref}/stripe-customer
Link the buyer to a customer that already exists in the account's connected Stripe account. Stripe issues the account details a buyer pays a bank transfer into per customer, so after this the buyer's next bank transfer goes to that customer's account details. Tito checks with Stripe that the customer exists and hasn't been deleted, and changes nothing if Stripe can't be reached (502 stripe_unavailable). The customer's email in Stripe is left as it is.
A customer can belong to one buyer only (409 stripe_customer_taken, naming the other buyer's ref). A buyer with a Stripe bank transfer still awaiting payment can't be linked to another customer until that transfer is paid or canceled (409 bank_transfer_open). Linking the customer the buyer already has changes nothing. There is no unlinking: an empty stripe_customer_id answers 422.
Requires payment.manage
Path parameters
refstring required
Request body
Response fields
refstringemailstringnamestringorgstringphonestring- The buyer's phone number as it was typed, trimmed and cut to 64 characters. It isn't checked or reformatted. Empty when the buyer has none.
quote_countinteger- Quotes matching this buyer's id OR their email — an anonymous quote is still theirs.
order_countinteger- Orders matching their email, case-insensitively: buyers.email is lowercased on the way in and an order keeps the address as the buyer typed it, so a case-sensitive match once read a registered buyer as having no orders at all.
part_ofstring or null- The ref of the buyer this one is part of, like a department's company, or null when it is part of nothing. One level: a buyer others are part of is never part of another.
kindobject or null- The kind a buyer is, or null for none. A kind is a label for finding and filtering buyers; nothing else depends on it. The whole kind is
GET /buyer-kinds/{id}. kind.idintegerkind.namestringcreated_atstring (RFC 3339) or nulladmin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
stripe_customer_idstring- The Customer this buyer is on Stripe, empty until something mints one (the bank-transfer path does it lazily). An identifier, not a credential.
people[].emailstringpeople[].primaryboolean- The buyer's primary contact: the address it is listed under, the one invoices and Stripe use. It can't be removed; make someone else primary first (
POST /buyers/{ref}/primary). people[].rolestring- What this person can do when the buyer's quotes need approval: a requester sends a quote for approval, an approver can reject one. Where approval isn't needed the two can do the same things.
- one of
requesterapprover people[].sees_allboolean- Whether this person sees every one of the buyer's bookings and quotes when they sign in, or only the ones made with their own email address. Only the organizer can change it (
POST /buyers/{ref}/people/{email}/sees). domainsarray of string- The buyer's email domains, sorted, never null. Addresses at exactly these domains belong to the buyer before anybody signs in.
termsobject or null- The named payment terms a buyer has of its own, or null when it has none. A buyer without terms of its own follows the terms of the buyer it is part of (
inherited_termson the buyer), or else each event's own bank transfer settings. The whole set isGET /payment-terms/{id}. terms.idintegerterms.namestringinherited_termsobject- What a buyer without terms of its own follows: the terms of the buyer it is part of. Null when it has its own, is part of nothing, or that buyer has none either.
inherited_terms.idintegerinherited_terms.namestringincludes[].refstringincludes[].namestringpo_requiredboolean- This buyer's own approval setting: its quotes, and those of every buyer part of it, go to an approver before they can become orders. Always false on a buyer that is part of another, which follows that buyer's;
approvalsays what applies. Set it withPOST /buyers/{ref}/po-required. approvalobject- Approval as it applies to this buyer: required or not, and the buyer whose approvers answer, which is the buyer this one is part of, or else itself.
approval.requiredbooleanapproval.approverobjectapproval.approver.refstringapproval.approver.namestringblockobject- Whether the buyer is blocked from starting new orders and quotes, by its own block or the one on the buyer it is part of. Set it with
POST /buyers/{ref}/blockand lift it withDELETE /buyers/{ref}/block. block.blockedbooleanblock.messagestring- The words emailed to whoever at the buyer tries to order, as plain text, empty for the standard line.
block.blocked_atstring (RFC 3339) or null- When the block was set, null when the buyer isn't blocked.
block.byobject or null- The buyer whose block applies: this one, or the one it is part of. Null when the buyer isn't blocked.
block.by.refstringblock.by.namestringspendarray of object- What this buyer has actually PAID, per currency: order totals less whatever was refunded off them. A buyer can span currencies across events, so it is a list. A partial refund leaves the order paid and is netted; a full one flips its status and nets to zero either way.
spend[].centsintegerspend[].currencystringquotesarray of BuyerQuoteSummaryquotes[].refstringquotes[].event_slugstringquotes[].statusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
quotes[].amount_centsintegerquotes[].currencystringquotes[].expires_atstring (RFC 3339) or nullquotes[].created_atstring (RFC 3339) or nullquotes[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
ordersarray of BuyerOrderSummaryorders[].refstringorders[].event_slugstringorders[].statusstringorders[].amount_centsintegerorders[].currencystringorders[].payment_methodstringorders[].created_atstring (RFC 3339) or nullorders[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
bank_transferobject or null- The newest wire-instruction snapshot, null until this buyer has one. Read from the stored row — no Stripe call — because the virtual IBAN is per-Customer, so that row IS the account they pay into. The IBAN is here deliberately: it is the account's own receiving account, printed on the invoice the buyer was already sent, not a secret that opens anything.
bank_transfer.referencestringbank_transfer.ibanstringbank_transfer.bicstringbank_transfer.account_holderstringbank_transfer.bank_countrystringbank_transfer.amount_centsintegerbank_transfer.currencystringbank_transfer.due_atstring (RFC 3339) or nullbank_detailsobject or null- The bank details Stripe issued this buyer's current Stripe customer, kept on the buyer: the account the buyer pays every bank transfer into, and what its vendor form prints. Null until they are kept, either by
POST /buyers/{ref}/bank-detailsor by the buyer's first Stripe bank transfer. Details kept for a Stripe customer the buyer is no longer linked to are never served. Read from the stored row, with no Stripe call. bank_details.account_holderstringbank_details.ibanstringbank_details.bicstringbank_details.currencystringbillingobject- This customer's billing identity, as currently stored (never a computed fallback — a blank
namehere can still print asorgon an invoice). billing.namestring- The legal entity an invoice is addressed to. Blank falls back to
orgwhen an invoice is rendered. billing.line1stringbilling.line2stringbilling.citystringbilling.postcodestringbilling.vat_numberstring
Responses
200The buyer as it now stands, with itsstripe_customer_id.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Another buyer has that customer (stripe_customer_taken), this buyer has a Stripe bank transfer awaiting payment (bank_transfer_open), the buyer's customer changed while the request was in flight (stripe_customer_moved), or the account has no Stripe connection (not_connected).422The body isn't{"stripe_customer_id": "cus_..."}(invalid_body), the id isn't shaped like a Stripe customer id (invalid_stripe_customer_id), Stripe has no customer with that id on this account (unknown_stripe_customer), or it has been deleted in Stripe (stripe_customer_deleted).500Internal error.502Stripe couldn't be asked about the customer, and nothing was changed (stripe_unavailable). Safe to retry. Stripe refusing this account's request answersstripe_refusedinstead, and nothing was changed either; check the account's Stripe connection before retrying.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/stripe-customer \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"stripe_customer_id":"cus_Q1w2E3r4T5y6U7"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/stripe-customer", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"stripe_customer_id": "cus_Q1w2E3r4T5y6U7"
}),
});
const data = await res.json();
{
"ref": "b7x9k2",
"email": "accounts@example.com",
"name": "Alex Smith",
"org": "Example Ltd",
"phone": "+1 202 555 0143",
"quote_count": 3,
"order_count": 5,
"part_of": "k3m8p1",
"kind": {
"id": 2,
"name": "Partners"
},
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito",
"anonymized_at": null,
"stripe_customer_id": "cus_1MEXAMPLE",
"people": [
{
"email": "accounts@example.com",
"primary": true,
"role": "approver",
"sees_all": true
}
],
"domains": [
"example.org"
],
"terms": {
"id": 3,
"name": "Members"
},
"inherited_terms": {
"id": 3,
"name": "Members"
},
"includes": [
{
"ref": "p2q7r4",
"name": "Example Ltd Design"
}
],
"po_required": false,
"approval": {
"required": true,
"approver": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
},
"block": {
"blocked": true,
"message": "Please settle your outstanding invoices with our accounts team before booking again.",
"blocked_at": "2026-10-21T11:30:00Z",
"by": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
},
"spend": [
{
"cents": 150000,
"currency": "eur"
}
],
"quotes": [
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "open",
"amount_cents": 14900,
"currency": "eur",
"order_ref": "H7K2PX",
"expires_at": "2026-10-14T09:00:00Z",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
],
"orders": [
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "paid",
"amount_cents": 14900,
"currency": "eur",
"payment_method": "example",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
],
"bank_transfer": {
"reference": "RF97TESTREF",
"iban": "example",
"bic": "example",
"account_holder": "Ada Lovelace",
"bank_country": "example",
"amount_cents": 14900,
"currency": "eur",
"status": "awaiting",
"due_at": "2026-10-14T09:00:00Z"
},
"bank_details": {
"account_holder": "Stripe Payments Europe",
"iban": "IE29AIBK93115212345678",
"bic": "AIBKIE2D",
"bank_country": "IE",
"currency": "eur"
},
"billing": {
"name": "St Brigid's NS Board of Management",
"line1": "12 Main Street",
"line2": "",
"city": "Springfield",
"postcode": "D02 AF30",
"subdivision": "Co. Dublin",
"country": "IE",
"vat_number": "IE1234567T",
"buyer_reference": "",
"po_reference": ""
}
}
Give a buyer payment terms
POST /buyers/{ref}/terms
Choose which payment terms this buyer's next invoices are issued under. 0 puts the buyer back on each event's own bank transfer settings. An invoice already issued keeps the terms it was issued under. A terms_id that names no payment terms answers 422 unknown_terms.
Requires orders.manage
Path parameters
refstring required
Request body
Response fields
termsobject or null- The named payment terms a buyer has of its own, or null when it has none. A buyer without terms of its own follows the terms of the buyer it is part of (
inherited_termson the buyer), or else each event's own bank transfer settings. The whole set isGET /payment-terms/{id}. terms.idintegerterms.namestring
Responses
200The buyer's terms as they now stand.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body isn't{"terms_id": n}(invalid_body), or no payment terms have that id (unknown_terms).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/terms \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"terms_id":3}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/buyers/H7K2PX/terms", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"terms_id": 3
}),
});
const data = await res.json();
{
"terms": {
"id": 3,
"name": "Members"
}
}
Payment terms
List payment terms
GET /payment-terms
The account's named payment terms, by name, each with how many buyers have it. A buyer with none follows each event's own bank transfer settings. There is no paging: an account has a handful.
Requires orders.view
Response fields
payment_termsarray of PaymentTermspayment_terms[].idintegerpayment_terms[].anchorstring- What
dayscounts from.ordercounts forward from the order.eventcounts forward from the day the event ends (or starts, when it has no end); an order that comes in later than that counts from the order instead, so nobody is due before they booked.before_eventcounts back from the day the event starts; an order that comes in later than that is due at the end of the day it was placed. An event with no date at all can't have an invoice issued under either event anchor. - one of
ordereventbefore_event payment_terms[].reminder_daysstring- Remind the buyer this many days before the due date, comma-separated. Empty for no reminders.
payment_terms[].overdue_daysstring- Chase the buyer this many days after the due date, comma-separated. Empty for no chasing.
payment_terms[].auto_cancel_daysinteger- Cancel the order this many days after the due date, putting its tickets back on sale. 0 is off.
payment_terms[].buyer_countinteger- How many buyers' invoices are issued under these terms: those with them as their own, and those without terms of their own that are part of one that has them.
Responses
200The payment terms.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/payment-terms \
-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/payment-terms", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"payment_terms": [
{
"id": 3,
"name": "Members",
"anchor": "event",
"days": 7,
"reminder_days": "7,1",
"overdue_days": "1,7",
"auto_cancel_days": 0,
"buyer_count": 12
}
]
}
Create payment terms
POST /payment-terms
Add a named set of payment terms. name, anchor and days are required. The reminder schedules are tidied the way an event's are (numbers, deduplicated, in order), and auto_cancel_days above 365 is held at 365. Answers with the terms as stored.
Requires settings.manage
Request body
namestring requireddaysinteger requiredreminder_daysstringoverdue_daysstringauto_cancel_daysinteger
Response fields
idintegeranchorstring- What
dayscounts from.ordercounts forward from the order.eventcounts forward from the day the event ends (or starts, when it has no end); an order that comes in later than that counts from the order instead, so nobody is due before they booked.before_eventcounts back from the day the event starts; an order that comes in later than that is due at the end of the day it was placed. An event with no date at all can't have an invoice issued under either event anchor. - one of
ordereventbefore_event reminder_daysstring- Remind the buyer this many days before the due date, comma-separated. Empty for no reminders.
overdue_daysstring- Chase the buyer this many days after the due date, comma-separated. Empty for no chasing.
auto_cancel_daysinteger- Cancel the order this many days after the due date, putting its tickets back on sale. 0 is off.
buyer_countinteger- How many buyers' invoices are issued under these terms: those with them as their own, and those without terms of their own that are part of one that has them.
Responses
201The payment terms were created.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422The body isn't an object of these fields (invalid_body), the name is blank (invalid_name), the anchor isn'torder,eventorbefore_event(invalid_anchor), or days is outside 1 to 365 (invalid_days).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/payment-terms \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Members","anchor":"event","days":7}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/payment-terms", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Members",
"anchor": "event",
"days": 7
}),
});
const data = await res.json();
{
"id": 3,
"name": "Members",
"anchor": "event",
"days": 7,
"reminder_days": "7,1",
"overdue_days": "1,7",
"auto_cancel_days": 0,
"buyer_count": 12
}
Remove payment terms
DELETE /payment-terms/{id}
Remove a set of payment terms. Every buyer on them goes back to each event's own bank transfer settings, in the same step; no buyer is removed. Invoices already issued keep the terms they were issued under. Answers with how many buyers were moved.
Requires settings.manage
Path parameters
idinteger required
Response fields
Responses
200The payment terms were removed.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/payment-terms/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/payment-terms/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"deleted": true,
"buyers_reset": 12
}
One set of payment terms
GET /payment-terms/{id}
One set of payment terms, with how many buyers have it.
Requires orders.view
Path parameters
idinteger required
Response fields
idintegeranchorstring- What
dayscounts from.ordercounts forward from the order.eventcounts forward from the day the event ends (or starts, when it has no end); an order that comes in later than that counts from the order instead, so nobody is due before they booked.before_eventcounts back from the day the event starts; an order that comes in later than that is due at the end of the day it was placed. An event with no date at all can't have an invoice issued under either event anchor. - one of
ordereventbefore_event reminder_daysstring- Remind the buyer this many days before the due date, comma-separated. Empty for no reminders.
overdue_daysstring- Chase the buyer this many days after the due date, comma-separated. Empty for no chasing.
auto_cancel_daysinteger- Cancel the order this many days after the due date, putting its tickets back on sale. 0 is off.
buyer_countinteger- How many buyers' invoices are issued under these terms: those with them as their own, and those without terms of their own that are part of one that has them.
Responses
200The payment terms.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/payment-terms/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/payment-terms/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"name": "Members",
"anchor": "event",
"days": 7,
"reminder_days": "7,1",
"overdue_days": "1,7",
"auto_cancel_days": 0,
"buyer_count": 12
}
Change payment terms
PATCH /payment-terms/{id}
Change a set of payment terms. A field left out stays as it is. The next invoice for each buyer on these terms is issued under the new values; an invoice already issued keeps the terms it was issued under, so no due date that has been sent moves.
Requires settings.manage
Path parameters
idinteger required
Request body
Response fields
idintegeranchorstring- What
dayscounts from.ordercounts forward from the order.eventcounts forward from the day the event ends (or starts, when it has no end); an order that comes in later than that counts from the order instead, so nobody is due before they booked.before_eventcounts back from the day the event starts; an order that comes in later than that is due at the end of the day it was placed. An event with no date at all can't have an invoice issued under either event anchor. - one of
ordereventbefore_event reminder_daysstring- Remind the buyer this many days before the due date, comma-separated. Empty for no reminders.
overdue_daysstring- Chase the buyer this many days after the due date, comma-separated. Empty for no chasing.
auto_cancel_daysinteger- Cancel the order this many days after the due date, putting its tickets back on sale. 0 is off.
buyer_countinteger- How many buyers' invoices are issued under these terms: those with them as their own, and those without terms of their own that are part of one that has them.
Responses
200The payment terms as they now stand.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body isn't an object of these fields (invalid_body), the name is blank (invalid_name), the anchor isn'torder,eventorbefore_event(invalid_anchor), or days is outside 1 to 365 (invalid_days).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/payment-terms/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Members"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/payment-terms/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Members"
}),
});
const data = await res.json();
{
"id": 3,
"name": "Members",
"anchor": "event",
"days": 7,
"reminder_days": "7,1",
"overdue_days": "1,7",
"auto_cancel_days": 0,
"buyer_count": 12
}
Contacts
The lists
GET /contact-lists
Active lists before archived ones, newest first within each, with each one's member count.
Requires contacts.view
Response fields
listsarray of ContactListlists[].idintegerlists[].namestringlists[].archivedbooleanlists[].member_countintegerlists[].created_atstring (RFC 3339)
Responses
200The lists.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404This account does not keep an address book (contacts_off), or the thing named does not exist (not_found). The switch isPATCH /contact-settings.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/contact-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/contact-lists", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"lists": [
{
"id": 4,
"name": "Newsletter",
"archived": false,
"member_count": 128,
"created_at": "2026-10-14T09:00:00Z"
}
]
}
Make a list
POST /contact-lists
A list needs a name and nothing else. A new list is never archived: make it, then PATCH it.
Requires contacts.manage
Request body
namestring
Response fields
Responses
201The list.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404This account does not keep an address book (contacts_off), or the thing named does not exist (not_found). The switch isPATCH /contact-settings.422A list needs a name (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/contact-lists \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Newsletter"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/contact-lists", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Newsletter"
}),
});
const data = await res.json();
{
"id": 4,
"name": "Newsletter",
"archived": false,
"member_count": 128,
"created_at": "2026-10-14T09:00:00Z"
}
One list
GET /contact-lists/{id}
One list in the account's address book, by id. Answers 404 with contacts_off when the account keeps no address book.
Requires contacts.view
Path parameters
idinteger required- The list's id.
Response fields
Responses
200The list.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404This account does not keep an address book (contacts_off), or the thing named does not exist (not_found). The switch isPATCH /contact-settings.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/contact-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/contact-lists/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 4,
"name": "Newsletter",
"archived": false,
"member_count": 128,
"created_at": "2026-10-14T09:00:00Z"
}
Rename or archive a list
PATCH /contact-lists/{id}
Archiving is how a list is retired — there is no delete, because a list somebody was added to is a record of who was told what.
Requires contacts.manage
Path parameters
idinteger required- The list's id.
Request body
namestring
Response fields
Responses
200The list as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404This account does not keep an address book (contacts_off), or the thing named does not exist (not_found). The switch isPATCH /contact-settings.422A list needs a name (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/contact-lists/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Newsletter"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/contact-lists/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Newsletter"
}),
});
const data = await res.json();
{
"id": 4,
"name": "Newsletter",
"archived": false,
"member_count": 128,
"created_at": "2026-10-14T09:00:00Z"
}
Who is on a list
GET /contact-lists/{id}/members
A contact's activity is not served here. The organizer's own page shows the orders, tickets and invitations matching their address — records that orders.view and attendees.view gate. A key holding only the address book must not read the account's sales through it, so neither the feed nor the per-person counts are on these answers.
Requires contacts.view
Path parameters
idinteger required- The list's id.
Response fields
contactsarray of Contactcontacts[].emailstringcontacts[].namestringcontacts[].orgstringcontacts[].phonestringcontacts[].notestringcontacts[].sourcestring- How this person first arrived:
manualfor somebody added deliberately, or the sweep's own word for an order, a ticket or an invitation. Stamped on first sighting and never rewritten. contacts[].tagsarray of string- Lowercased and trimmed on the way in. Organizer data — never translated.
contacts[].listsarray of ContactListcontacts[].lists[].idintegercontacts[].lists[].namestringcontacts[].lists[].archivedbooleancontacts[].lists[].member_countintegercontacts[].lists[].created_atstring (RFC 3339)contacts[].created_atstring (RFC 3339)contacts[].updated_atstring (RFC 3339)
Responses
200The people on it.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404This account does not keep an address book (contacts_off), or the thing named does not exist (not_found). The switch isPATCH /contact-settings.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/contact-lists/1/members \
-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/contact-lists/1/members", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"contacts": [
{
"ref": "c1a2b3",
"email": "ada@example.com",
"name": "Ada Lovelace",
"org": "Analytical Engines",
"phone": "example",
"note": "example",
"source": "manual",
"tags": [
"speaker"
],
"lists": [
{
"id": 4,
"name": "Newsletter",
"archived": false,
"member_count": 128,
"created_at": "2026-10-14T09:00:00Z"
}
],
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z"
}
]
}
Put people on a list
POST /contact-lists/{id}/members
By ref, in bulk. The answer says how many were actually ADDED — the difference being those who were already on it, so a repeated call is safe and says 0.
An archived list refuses. It is off every chooser the organizer has, so this would otherwise be the one way to grow a list nobody can see.
Requires contacts.manage
Path parameters
idinteger required- The list's id.
Request body
contactsarray of string required
Response fields
addedinteger
Responses
200How many were added.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404This account does not keep an address book (contacts_off), or the thing named does not exist (not_found). The switch isPATCH /contact-settings.409That list is archived (list_archived).422A ref that is nobody (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/contact-lists/1/members \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"contacts":["example"]}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/contact-lists/1/members", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"contacts": [
"example"
]
}),
});
const data = await res.json();
{
"added": 2
}
Take somebody off a list
DELETE /contact-lists/{id}/members/{ref}
Allowed on an ARCHIVED list too, deliberately: taking somebody off a list they should not be on is not a change to the list.
Requires contacts.manage
Path parameters
idinteger required- The list's id.
refstring required- The contact's ref.
Response fields
removedboolean
Responses
200They are off it.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404This account does not keep an address book (contacts_off), or the thing named does not exist (not_found). The switch isPATCH /contact-settings.500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/contact-lists/1/members/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/contact-lists/1/members/H7K2PX", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"removed": true
}
Get the contacts settings
GET /contact-settings
Whether the account derives an address book from its own activity, and from which sources. This is the SETTINGS panel, not the address book: the contacts themselves are their own endpoints and their own capability, because a switch deciding whether a CRM exists is a different thing from the people in it.
The master switch is DEFAULT OFF: building a CRM out of buyer data is opt-in, because the organizer is the data controller. The three source switches are the other way round — unset means on, so turning the feature on without naming them collects from everywhere.
contact_count is how many contacts the account now holds — it is on this shape because switching the feature on runs the backfill, and this is what says whether that found anything.
Requires settings.manage
Response fields
from_ordersboolean- Buyers of pending and paid orders. A canceled order is an abandoned checkout and mints nothing.
contact_countinteger- How many contacts the account holds. Switching the feature on runs the backfill, so this is what says whether it found anything.
Responses
200The contacts settings.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/contact-settings \
-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/contact-settings", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"on": true,
"from_orders": true,
"from_tickets": true,
"from_invitations": true,
"contact_count": 412
}
Change the contacts settings
PATCH /contact-settings
Only the keys present change; an empty body names nothing and so changes nothing.
Turning it on runs the backfill, inline. Contacts are derived from activity, so enabling the feature late still captures the history that came before — otherwise an organizer switches it on, sees nothing, and can only wait for the next order. The sweep is idempotent (fill-blanks-only), so asking again is a no-op rather than a duplicate, and PATCH {"on": true} on an already-on account is a legitimate way to ask for a re-derive.
The master switch is DEFAULT OFF: building a CRM out of buyer data is opt-in, because the organizer is the data controller. The three source switches are the other way round — unset means on, so turning the feature on without naming them collects from everywhere.
A source switched off means the sweep stops deriving from it; the contacts it already derived stay, because they are the account's record of real people rather than a cache of the switch.
Requires settings.manage
Request body
from_ordersboolean- Buyers of pending and paid orders. A canceled order is an abandoned checkout and mints nothing.
Response fields
from_ordersboolean- Buyers of pending and paid orders. A canceled order is an abandoned checkout and mints nothing.
contact_countinteger- How many contacts the account holds. Switching the feature on runs the backfill, so this is what says whether it found anything.
Responses
200The settings, in the shapeGET /contact-settingsanswers with — with acontact_countthat may have just changed.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422An explicit null, a switch that is not a boolean, or an unknown field (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/contact-settings \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"on":true}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/contact-settings", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"on": true
}),
});
const data = await res.json();
{
"on": true,
"from_orders": true,
"from_tickets": true,
"from_invitations": true,
"contact_count": 412
}
The address book
GET /contacts
Newest first. tag and list are the two narrowings the organizer's own list has, and they COMBINE; q matches an email, a name or an organization.
A contact's activity is not served here. The organizer's own page shows the orders, tickets and invitations matching their address — records that orders.view and attendees.view gate. A key holding only the address book must not read the account's sales through it, so neither the feed nor the per-person counts are on these answers.
Requires contacts.view
Query parameters
limitintegerafterstring- A contact ref: the page continues below it.
qstringtagstringlistinteger
Response fields
contactsarray of Contactcontacts[].emailstringcontacts[].namestringcontacts[].orgstringcontacts[].phonestringcontacts[].notestringcontacts[].sourcestring- How this person first arrived:
manualfor somebody added deliberately, or the sweep's own word for an order, a ticket or an invitation. Stamped on first sighting and never rewritten. contacts[].tagsarray of string- Lowercased and trimmed on the way in. Organizer data — never translated.
contacts[].listsarray of ContactListcontacts[].lists[].idintegercontacts[].lists[].namestringcontacts[].lists[].archivedbooleancontacts[].lists[].member_countintegercontacts[].lists[].created_atstring (RFC 3339)contacts[].created_atstring (RFC 3339)contacts[].updated_atstring (RFC 3339)
Responses
200A page of people.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404This account does not keep an address book (contacts_off), or the thing named does not exist (not_found). The switch isPATCH /contact-settings.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/contacts \
-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/contacts", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"contacts": [
{
"ref": "c1a2b3",
"email": "ada@example.com",
"name": "Ada Lovelace",
"org": "Analytical Engines",
"phone": "example",
"note": "example",
"source": "manual",
"tags": [
"speaker"
],
"lists": [
{
"id": 4,
"name": "Newsletter",
"archived": false,
"member_count": 128,
"created_at": "2026-10-14T09:00:00Z"
}
],
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z"
}
]
}
Add somebody
POST /contacts
Adds a person deliberately — source comes back as manual, which is what separates them from somebody the sweep found on an order.
An email already on file is refused, and the refusal names who is there. It is not an upsert: adopting the existing row and filling its blanks would answer 201 having quietly edited somebody else.
Requires contacts.manage
Request body
emailstringnamestringorgstringphonestringnotestringtagsarray of string- A SET: naming it replaces what is there, omitting it leaves it, and an empty array clears it.
listsarray of integer- A SET of list ids, the same way. Archived lists are refused: they are off every chooser the organizer has, so adding to one here would be the one way to grow a list nobody can see. Memberships of an archived list are left exactly as they stand.
Response fields
emailstringnamestringorgstringphonestringnotestringsourcestring- How this person first arrived:
manualfor somebody added deliberately, or the sweep's own word for an order, a ticket or an invitation. Stamped on first sighting and never rewritten. listsarray of ContactListlists[].idintegerlists[].namestringlists[].archivedbooleanlists[].member_countintegerlists[].created_atstring (RFC 3339)created_atstring (RFC 3339)updated_atstring (RFC 3339)
Responses
201The person.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404This account does not keep an address book (contacts_off), or the thing named does not exist (not_found). The switch isPATCH /contact-settings.409That email is already on file (email_on_file); the message names the contact.422Not an email address, or a list that is archived or does not exist (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/contacts \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/contacts", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"email": "ada@example.com"
}),
});
const data = await res.json();
{
"ref": "c1a2b3",
"email": "ada@example.com",
"name": "Ada Lovelace",
"org": "Analytical Engines",
"phone": "example",
"note": "example",
"source": "manual",
"tags": [
"speaker"
],
"lists": [
{
"id": 4,
"name": "Newsletter",
"archived": false,
"member_count": 128,
"created_at": "2026-10-14T09:00:00Z"
}
],
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z"
}
Delete somebody
DELETE /contacts/{ref}
Removes the address-book row and everything hanging off it — tags and memberships.
It never touches orders, tickets or invitations. Those are business records; only the overlay is deleted. Somebody deleted here reappears in the book the next time the sweep sees them on an order, which is the design rather than a leak.
Requires contacts.manage
Path parameters
refstring required- The contact's ref.
Response fields
deletedboolean
Responses
200They are gone from the book.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404This account does not keep an address book (contacts_off), or the thing named does not exist (not_found). The switch isPATCH /contact-settings.500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/contacts/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/contacts/H7K2PX", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"deleted": true
}
One person
GET /contacts/{ref}
A contact's activity is not served here. The organizer's own page shows the orders, tickets and invitations matching their address — records that orders.view and attendees.view gate. A key holding only the address book must not read the account's sales through it, so neither the feed nor the per-person counts are on these answers.
Requires contacts.view
Path parameters
refstring required- The contact's ref.
Response fields
emailstringnamestringorgstringphonestringnotestringsourcestring- How this person first arrived:
manualfor somebody added deliberately, or the sweep's own word for an order, a ticket or an invitation. Stamped on first sighting and never rewritten. listsarray of ContactListlists[].idintegerlists[].namestringlists[].archivedbooleanlists[].member_countintegerlists[].created_atstring (RFC 3339)created_atstring (RFC 3339)updated_atstring (RFC 3339)
Responses
200The person.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404This account does not keep an address book (contacts_off), or the thing named does not exist (not_found). The switch isPATCH /contact-settings.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/contacts/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/contacts/H7K2PX", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"ref": "c1a2b3",
"email": "ada@example.com",
"name": "Ada Lovelace",
"org": "Analytical Engines",
"phone": "example",
"note": "example",
"source": "manual",
"tags": [
"speaker"
],
"lists": [
{
"id": 4,
"name": "Newsletter",
"archived": false,
"member_count": 128,
"created_at": "2026-10-14T09:00:00Z"
}
],
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z"
}
Edit somebody
PATCH /contacts/{ref}
An organizer's edit is authoritative — unlike the sweep, which only ever fills blanks — so the fields named here overwrite. The ones not named keep what they hold.
Changing the email RE-KEYS the person; an address another contact already holds is refused.
Requires contacts.manage
Path parameters
refstring required- The contact's ref.
Request body
emailstringnamestringorgstringphonestringnotestringtagsarray of string- A SET: naming it replaces what is there, omitting it leaves it, and an empty array clears it.
listsarray of integer- A SET of list ids, the same way. Archived lists are refused: they are off every chooser the organizer has, so adding to one here would be the one way to grow a list nobody can see. Memberships of an archived list are left exactly as they stand.
Response fields
emailstringnamestringorgstringphonestringnotestringsourcestring- How this person first arrived:
manualfor somebody added deliberately, or the sweep's own word for an order, a ticket or an invitation. Stamped on first sighting and never rewritten. listsarray of ContactListlists[].idintegerlists[].namestringlists[].archivedbooleanlists[].member_countintegerlists[].created_atstring (RFC 3339)created_atstring (RFC 3339)updated_atstring (RFC 3339)
Responses
200The person as they now stand.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404This account does not keep an address book (contacts_off), or the thing named does not exist (not_found). The switch isPATCH /contact-settings.409Another contact already has that email (email_on_file).422Not an email address, or a list that is archived or does not exist (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/contacts/H7K2PX \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/contacts/H7K2PX", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"email": "ada@example.com"
}),
});
const data = await res.json();
{
"ref": "c1a2b3",
"email": "ada@example.com",
"name": "Ada Lovelace",
"org": "Analytical Engines",
"phone": "example",
"note": "example",
"source": "manual",
"tags": [
"speaker"
],
"lists": [
{
"id": 4,
"name": "Newsletter",
"archived": false,
"member_count": 128,
"created_at": "2026-10-14T09:00:00Z"
}
],
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z"
}
List unsubscribes
GET /unsubscribes
Everyone who has asked to stop getting the account's messages, newest first: from one event's messages, or from every message the account sends. Booking email (confirmations, tickets, invoices, refunds) still reaches them; only messages an organizer writes and sends stop. Read-only: only the person can subscribe again, from their own link. Paged by after, an unsubscribe id from the previous page's next_after. An agent limited to some events cannot call this.
Requires settings.manage
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
unsubscribesarray of Unsubscribeunsubscribes[].idintegerunsubscribes[].scopestringeventstops one event's messages;accountstops every message the account sends.- one of
eventaccount unsubscribes[].reasonstringunsubscribedis the person's own choice, and the only reason recorded today.- one of
unsubscribedbouncedcomplained unsubscribes[].message_idinteger or null- The id of the message whose link they used, one of the event's messages; null when there was none.
unsubscribes[].created_atstring (RFC 3339)
Responses
200A page of the list.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422afteris not an unsubscribe id (invalid_after).500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/unsubscribes \
-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/unsubscribes", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"unsubscribes": [
{
"id": 1,
"email": "ada@example.com",
"scope": "event",
"event": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"reason": "unsubscribed",
"message_id": 1,
"created_at": "2026-10-14T09:00:00Z"
}
],
"next_after": "example"
}
Details
List the account's details
GET /details
The account's DETAILS: what each event says about itself — its venue's parking, its dress code — and what an event that has not answered reports instead. This is the event-scope half of the fields table; the questions asked of PEOPLE are /fields, and neither endpoint's verbs reach the other's rows. Live details first, then archived ones — removing is a door that opens both ways, so what went through it stays visible. The DEFAULT resolves onto every event that has not answered for itself, so a value stored wrong reaches all of them at once. overrides is how many events have answered for themselves — an explicit blank included, since saying nothing on purpose is still an answer the default no longer reaches — and it is the one number that makes a default's reach legible before it is changed.
Requires events.view
Response fields
detailsarray of Detaildetails[].idintegerdetails[].labelstringdetails[].optionsarray of string- A select's choices, in order, as stored. Empty on every other kind.
details[].descriptionstringdetails[].defaultstring- What an event that has not answered reports. "" is a real state: no default at all.
details[].in_calendarboolean- Whether this detail's value goes into every calendar entry Tito writes for an event, below the event's description. A blank value is left out.
details[].overridesinteger- How many events have answered for themselves, an explicit blank included.
details[].answeredboolean- Whether anything has ever filled it in, which decides whether DELETE destroys or archives.
details[].archivedboolean
Responses
200The details.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/details \
-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/details", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"details": [
{
"id": 5,
"key": "dress_code",
"label": "Dress code",
"kind": "select",
"options": [
"Smart casual",
"Black tie"
],
"description": "What to wear",
"default": "Smart casual",
"in_calendar": false,
"overrides": 3,
"answered": true,
"position": 2,
"archived": false
}
]
}
Add a detail
POST /details
A new event detail. Its key is derived from the label and suffixed past a collision. kind is one this surface offers — text, textarea, number, yes_no or select — and a kind the table knows but the panel does not offer is refused, because a file detail has no upload surface here at all. A select's default must be one of its own options.
Requires events.manage
Request body
descriptionstringin_calendarboolean- Put this detail's value into every calendar entry written for an event. Defaults to false.
Response fields
idintegerlabelstringdescriptionstringin_calendarboolean- Whether this detail's value goes into every calendar entry Tito writes for an event, below the event's description. A blank value is left out.
answeredboolean- Whether anything has ever filled it in, which decides whether DELETE destroys or archives.
archivedboolean
Responses
201The detail as made.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422A blank label, a kind this surface does not offer, a number default that is not a number, or aselectdefault that is not one of its options (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/details \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"label":"Dress code","kind":"select"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/details", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"label": "Dress code",
"kind": "select"
}),
});
const data = await res.json();
{
"id": 5,
"key": "dress_code",
"label": "Dress code",
"kind": "select",
"options": [
"Smart casual",
"Black tie"
],
"description": "What to wear",
"default": "Smart casual",
"in_calendar": false,
"overrides": 3,
"answered": true,
"position": 2,
"archived": false
}
Remove a detail
DELETE /details/{id}
Does ONE OF TWO THINGS, and says which. A detail nothing has ever filled in is DESTROYED; one with answers behind it is ARCHIVED, because the answers would otherwise lose the question they were given to. answered on the read says which it will be before you ask.
Requires events.manage
Path parameters
idinteger required- The detail's id, as
GET /detailslists it.
Response fields
Responses
200What happened to it.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409It is archived already (archived).500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/details/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/details/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"deleted": false,
"archived": true
}
Get a detail
GET /details/{id}
One detail, archived ones included — an archived detail is restorable, so a caller has to be able to read it.
Requires events.view
Path parameters
idinteger required- The detail's id, as
GET /detailslists it.
Response fields
idintegerlabelstringdescriptionstringin_calendarboolean- Whether this detail's value goes into every calendar entry Tito writes for an event, below the event's description. A blank value is left out.
answeredboolean- Whether anything has ever filled it in, which decides whether DELETE destroys or archives.
archivedboolean
Responses
200The detail.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/details/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/details/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 5,
"key": "dress_code",
"label": "Dress code",
"kind": "select",
"options": [
"Smart casual",
"Black tie"
],
"description": "What to wear",
"default": "Smart casual",
"in_calendar": false,
"overrides": 3,
"answered": true,
"position": 2,
"archived": false
}
Change a detail's default or calendar tick
PATCH /details/{id}
The default and in_calendar are the only writable things, and that is the panel's own shape rather than a narrowing here: a detail's NAME and TYPE are not editable on any surface, because changing either would reach every event's copy of an answer already given. The DEFAULT resolves onto every event that has not answered for itself, so a value stored wrong reaches all of them at once. overrides is how many events have answered for themselves — an explicit blank included, since saying nothing on purpose is still an answer the default no longer reaches — and it is the one number that makes a default's reach legible before it is changed.
Requires events.manage
Path parameters
idinteger required- The detail's id, as
GET /detailslists it.
Request body
in_calendarboolean- Whether this detail's value goes into every calendar entry written for an event.
Response fields
idintegerlabelstringdescriptionstringin_calendarboolean- Whether this detail's value goes into every calendar entry Tito writes for an event, below the event's description. A blank value is left out.
answeredboolean- Whether anything has ever filled it in, which decides whether DELETE destroys or archives.
archivedboolean
Responses
200The detail as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The default is not a number, or not one of this detail's options (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/details/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"default":"Black tie"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/details/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"default": "Black tie"
}),
});
const data = await res.json();
{
"id": 5,
"key": "dress_code",
"label": "Dress code",
"kind": "select",
"options": [
"Smart casual",
"Black tie"
],
"description": "What to wear",
"default": "Smart casual",
"in_calendar": false,
"overrides": 3,
"answered": true,
"position": 2,
"archived": false
}
Restore an archived detail
POST /details/{id}/restore
The other way through the same door.
Requires events.manage
Path parameters
idinteger required- The detail's id, as
GET /detailslists it.
Response fields
idintegerlabelstringdescriptionstringin_calendarboolean- Whether this detail's value goes into every calendar entry Tito writes for an event, below the event's description. A blank value is left out.
answeredboolean- Whether anything has ever filled it in, which decides whether DELETE destroys or archives.
archivedboolean
Responses
200The detail, live again.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409It is not archived (not_archived).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/details/1/restore \
-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/details/1/restore", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 5,
"key": "dress_code",
"label": "Dress code",
"kind": "select",
"options": [
"Smart casual",
"Black tie"
],
"description": "What to wear",
"default": "Smart casual",
"in_calendar": false,
"overrides": 3,
"answered": true,
"position": 2,
"archived": false
}
Custom pages
List custom pages
GET /pages
List custom pages with their source, publication state, versions, timestamps, and editor and public links.
Requires settings.manage
Response fields
pagesarray of CustomPagepages[].idintegerpages[].pathstringpages[].titlestringpages[].publishedbooleanpages[].versionstringpages[].admin_urlstringpages[].public_urlstringpages[].created_atstring (RFC 3339)pages[].updated_atstring (RFC 3339)
Responses
200List custom pages401No 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/pages \
-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/pages", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"pages": [
{
"id": 1,
"path": "example",
"title": "example",
"body": "example",
"published": true,
"version": "example",
"admin_url": "https://example.com/hooks/tito",
"public_url": "https://example.com/hooks/tito",
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z"
}
]
}
Create a custom page
POST /pages
Create a draft unless published is explicitly true. The source must compile and the path must not claim a reserved, event, or series address.
Requires settings.manage
Request body
Response fields
idintegerpathstringtitlestringpublishedbooleanversionstringadmin_urlstringpublic_urlstringcreated_atstring (RFC 3339)updated_atstring (RFC 3339)
Responses
201Create a custom page401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid page source or address500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/pages \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"path":"example","title":"example"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/pages", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"path": "example",
"title": "example"
}),
});
const data = await res.json();
{
"id": 1,
"path": "example",
"title": "example",
"body": "example",
"published": true,
"version": "example",
"admin_url": "https://example.com/hooks/tito",
"public_url": "https://example.com/hooks/tito",
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z"
}
Delete a custom page
DELETE /pages/{id}
Delete the custom page.
Requires settings.manage
Path parameters
idinteger required
Responses
204Deleted401No 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/pages/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/pages/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
Get a custom page
GET /pages/{id}
Read one custom page, including its source and version.
Requires settings.manage
Path parameters
idinteger required
Response fields
idintegerpathstringtitlestringpublishedbooleanversionstringadmin_urlstringpublic_urlstringcreated_atstring (RFC 3339)updated_atstring (RFC 3339)
Responses
200Get a custom page401No 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/pages/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/pages/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 1,
"path": "example",
"title": "example",
"body": "example",
"published": true,
"version": "example",
"admin_url": "https://example.com/hooks/tito",
"public_url": "https://example.com/hooks/tito",
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z"
}
Update a custom page
PATCH /pages/{id}
Update path, title, body, or published. Include base from the last read to refuse a stale edit; a page changed during the update also returns 409.
Requires settings.manage
Path parameters
idinteger required
Request body
Response fields
idintegerpathstringtitlestringpublishedbooleanversionstringadmin_urlstringpublic_urlstringcreated_atstring (RFC 3339)updated_atstring (RFC 3339)
Responses
200Update a custom page401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The page changed since the supplied version or during the update422Invalid page source or address500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/pages/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"path":"example"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/pages/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"path": "example"
}),
});
const data = await res.json();
{
"id": 1,
"path": "example",
"title": "example",
"body": "example",
"published": true,
"version": "example",
"admin_url": "https://example.com/hooks/tito",
"public_url": "https://example.com/hooks/tito",
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z"
}
Design
The account's design
GET /design
The tokens the Customize studio edits, as the defaults every event inherits — with the staged value and the published one side by side.
Every edit is a draft edit. The first change copies the published design to a draft, every change after it writes that draft, and POST /design/publish is the single moment any of it reaches a buyer. A caller that never publishes has changed nothing public.
A stored value that no longer validates is absent rather than served: these are re-checked on the way out as well as on the way in, so nothing account-shaped can reach a stylesheet through a hand-edited row.
Requires settings.manage
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The design.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/design \
-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/design", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
Stage design changes
PATCH /design
Every edit is a draft edit. The first change copies the published design to a draft, every change after it writes that draft, and POST /design/publish is the single moment any of it reaches a buyer. A caller that never publishes has changed nothing public.
The whole body is checked before anything is written. The studio can refuse one control at a time because a person is looking at the others; a caller sending six tokens and getting a 422 for the fourth would have to work out which of the six took.
The event pages' layout is seven tokens: layout_header (banner, plain, split, masthead, centered or bleed), layout_cards, what is on a card on the event page (one; below for the name, date and description on the page and the rest on a card under them; or each for the same with each section on a card of its own), layout_date, where the date and place go (line, art, blocks, pills or lines; the masthead keeps its own dateline whatever this says), layout_tickets (list, cards, table, sheet or menu; the table and the spreadsheet add a heading row on the event page, and the widget always shows the list), layout_total (panel, receipt or bar), layout_spacing, the space around the event page and between the tickets (compact, standard or airy), and layout_ticket, the ticket's own page (stub, admission, pass or slip). card_price_ink is the price color on ticket cards (heading or text), and page_width is the event page's width (fit, wide only when something on it has a price, or wide, always, except under the centered header, which keeps its narrower page). Each takes the same draft and publish as every other token. A design that started from Confetti, Sprinkles' old name, reads as sprinkles, and one that started from Homage, Classic's, reads as classic.
Requires settings.manage
Request body
tokensobject- Values, keyed as
GET /designlists them. Each is validated by its kind, in the same words the studio would use. Abooltoken takes "1" or "0", or "true" or "false", which are stored as "1" and "0". resetarray of string- Tokens to take back to the layer below — the look's, or the product default. A list of its own rather than a null in
tokens, because for theurlandimagekinds an empty string is a REAL value ("no image"), so clearing and unsetting are two different acts.
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The design, with the staged values in place.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422A token this build does not have, or a value its kind refuses — a bad hex, a radius out of range, a choice outside its set (invalid_body). The message names the token.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/design \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"tokens":null}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/design", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"tokens": null
}),
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
Read the account stylesheet
GET /design/css
Returns only this scope’s own CSS, not inherited CSS.
Requires settings.manage
Response fields
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
Responses
200The draft and published stylesheet for this scope.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/design/css \
-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/design/css", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
Set the account stylesheet
PUT /design/css
Saves only to the shared Customize draft. Publish through the account design route to make it public. The stylesheet is served as written on the account’s own address, and on the root domain with anything that loads from another address blocked; draft_blocked and published_blocked say what.
Requires settings.manage
Request body
Response fields
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
Responses
200The draft and published stylesheet for this scope.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422Invalid or oversized CSS body.500Internal error.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/design/css \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"css":"example"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/design/css", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"css": "example"
}),
});
const data = await res.json();
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
Throw the staged design away
POST /design/discard
Drops every staged change. The published design is untouched, so nothing is purged and no buyer sees a thing.
Requires settings.manage
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The design, back to what is published.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/design/discard \
-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/design/discard", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
Read the account documents’ stylesheet
GET /design/document-css
Returns only this scope’s own CSS, not inherited CSS.
Requires settings.manage
Response fields
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
Responses
200The draft and published stylesheet for this scope.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/design/document-css \
-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/design/document-css", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
Set the account documents’ stylesheet
PUT /design/document-css
Saves only to the shared Customize draft. Publish through the account design route to make it public. The documents’ stylesheet reaches the invoice, the receipt, the refund receipt and the printed ticket, and nothing else; the pages’ stylesheet never reaches them. On a document, anything that loads from another address is left out on every host, and draft_blocked and published_blocked say what.
Requires settings.manage
Request body
Response fields
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
Responses
200The draft and published stylesheet for this scope.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422Invalid or oversized CSS body.500Internal error.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/design/document-css \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"css":"example"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/design/document-css", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"css": "example"
}),
});
const data = await res.json();
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
Read the account emails’ stylesheet
GET /design/email-css
Returns only this scope’s own CSS, not inherited CSS.
Requires settings.manage
Response fields
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
Responses
200The draft and published stylesheet for this scope.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/design/email-css \
-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/design/email-css", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
Set the account emails’ stylesheet
PUT /design/email-css
Saves only to the shared Customize draft. Publish through the account design route to make it public. The emails’ stylesheet reaches every email sent about an event, and nothing else; the pages’ stylesheet never reaches an email. In an email only rules that name one of the email classes (email-card, email-button and the rest, listed in the CSS contract) are kept, every declaration is made !important so it wins over the email’s inline styles, and anything that loads from another address is left out. draft_blocked and published_blocked say what was left out for loading.
Requires settings.manage
Request body
Response fields
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
Responses
200The draft and published stylesheet for this scope.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422Invalid or oversized CSS body.500Internal error.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/design/email-css \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"css":"example"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/design/email-css", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"css": "example"
}),
});
const data = await res.json();
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
Start every event's look from your own website
POST /design/from-site
Reads the website's home page and up to four of its stylesheets, and writes a draft from it into the account's design draft: Paper's settings, with the site's colors, the closest of the hosted fonts to its own, its logo (stored as an uploaded image), its header and footer links in site_header and site_footer, and the layout its shape suits. Only public https addresses on the default port are read, each read is capped in size and time, and an account can read 6 websites in 10 minutes. Nothing from the website is stored as markup or CSS. Nothing is published: POST /design/publish does that.
Requires settings.manage
Request body
urlstring required- The website to read: a host, or an address starting https:// (http:// is read as https://).
layoutboolean- Write the layout the site's shape suits:
layout_header,layout_ticketsandlayout_spacingwhere the home page says, and Paper's for the other layout parts. False, or a site whose shape can't be told, leaves the layout as it is. A picture across the whole window brings no artwork through the API.
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The design, with the website's draft in its draft.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422The website couldn't be used:site_err_address(not a website address),site_err_private(not a public address),site_err_unreachable,site_err_refused(the site turned the request away),site_err_slow(over 10 seconds),site_err_notpage(not HTML, or over 2 MB),site_err_nothing(nothing usable found), or an unknown field (invalid_body).429More than 6 websites read in 10 minutes (rate_limited).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/design/from-site \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.example.com"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/design/from-site", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://www.example.com"
}),
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
Publish the staged design
POST /design/publish
The one moment any of this reaches a buyer. Publishing purges every public shell, because account-scope tokens restyle every public page — the same blast radius as assigning a look. Discarding purges nothing: the published design never moved.
Publishing with nothing staged is not an error: it is a no-op answering the same shape, so a caller need not track whether its own last change staged anything.
Requires settings.manage
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The design, now published.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/design/publish \
-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/design/publish", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
Start every event's look from a theme
POST /design/theme
Writes the theme's colors, fonts, buttons, cards, headings and decoration into the account's design draft, and records the theme as look_theme. Banner artwork belongs to each event, so it comes with a theme started from on an event. The logo's place and the account's own stylesheet are never changed. Nothing is published: POST /design/publish does that.
Requires settings.manage
Request body
themestring required- The theme to start from: one of the
look_themetoken's options in the design (paper, classic, ledger, gazette, midnight, signal, fieldwork, pop, riso, afterglow, arcade, sprinkles). Starting from one writes its layout tokens too.
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The design, with the theme in its draft.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422A theme this build does not have, or an unknown field (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/design/theme \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"theme":"riso"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/design/theme", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"theme": "riso"
}),
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
get account wording
GET /design/wording
The words on the account's pages that can be changed, in one language (locale, en by default): each line's key, the screens it appears on, and its draft and published text.
Requires settings.manage
Query parameters
localestring
Response fields
localestringaudiencestring- The audience slug whose own wording
draftandpublishedcarry, or an empty string for everyone's wording. termsinteger- The id of the payment terms whose own wording
draftandpublishedcarry, or 0 for everyone's wording. Never set beside a non-emptyaudience. draft_openbooleanwordsarray of DesignWordingRowwords[].keystringwords[].screensarray of stringwords[].formsarray of stringwords[].hideablebooleanwords[].formattableboolean- The line may carry bold, italic, bullets and links. Once formatted, its draft and published values are a composer body rather than a plain sentence.
words[].per_audienceboolean- An audience of the event can have its own wording for this line, on top of everyone's. Only these lines are listed with
?audience=, and only these acceptaudiencein a change. words[].per_termsboolean- A set of payment terms can have its own wording for this line, on top of everyone's: the lines of the invoice emails. Only these lines are listed with
?terms=, and only these accepttermsin a change. No line is bothper_audienceandper_terms. words[].draftobjectwords[].publishedobjectwords[].argsarray of string
Responses
200The wording for this scope and locale.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422Invalid locale (invalid_locale). Account wording has no audience's or payment terms' layer, soaudience(invalid_audience) andterms(invalid_terms) are refused here.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/design/wording \
-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/design/wording", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"locale": "example",
"audience": "example",
"terms": 1,
"draft_open": true,
"words": [
{
"key": "example",
"screens": [
"example"
],
"forms": [
"example"
],
"hideable": true,
"formattable": true,
"per_audience": true,
"per_terms": true,
"draft": {},
"published": {},
"draft_hidden": true,
"published_hidden": true,
"args": [
"example"
]
}
]
}
set account wording
PATCH /design/wording
Changes one line of the account's wording, in one language, in the design draft. The live pages don't change until the draft is published with POST /design/publish.
Requires settings.manage
Request body
localestring requiredkeystring requiredformstringvaluestring requiredaudiencestring- An event's audience slug: changes that audience's own wording for the line instead of everyone's. The line must be one with
per_audiencetrue. Not accepted for account wording, and not withterms. termsinteger- The id of a set of payment terms (
GET /payment-terms): changes those terms' own wording for the line instead of everyone's. The line must be one withper_termstrue. Not accepted for account wording, and not withaudience.
Response fields
localestringaudiencestring- The audience slug whose own wording
draftandpublishedcarry, or an empty string for everyone's wording. termsinteger- The id of the payment terms whose own wording
draftandpublishedcarry, or 0 for everyone's wording. Never set beside a non-emptyaudience. draft_openbooleanwordsarray of DesignWordingRowwords[].keystringwords[].screensarray of stringwords[].formsarray of stringwords[].hideablebooleanwords[].formattableboolean- The line may carry bold, italic, bullets and links. Once formatted, its draft and published values are a composer body rather than a plain sentence.
words[].per_audienceboolean- An audience of the event can have its own wording for this line, on top of everyone's. Only these lines are listed with
?audience=, and only these acceptaudiencein a change. words[].per_termsboolean- A set of payment terms can have its own wording for this line, on top of everyone's: the lines of the invoice emails. Only these lines are listed with
?terms=, and only these accepttermsin a change. No line is bothper_audienceandper_terms. words[].draftobjectwords[].publishedobjectwords[].argsarray of string
Responses
200The wording for this scope and locale.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422Invalid wording input (invalid_body). Account wording has no audience's or payment terms' layer, soaudience(invalid_audience) andterms(invalid_terms) are refused here.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/design/wording \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"locale":"example","key":"example","value":"example"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/design/wording", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"locale": "example",
"key": "example",
"value": "example"
}),
});
const data = await res.json();
{
"locale": "example",
"audience": "example",
"terms": 1,
"draft_open": true,
"words": [
{
"key": "example",
"screens": [
"example"
],
"forms": [
"example"
],
"hideable": true,
"formattable": true,
"per_audience": true,
"per_terms": true,
"draft": {},
"published": {},
"draft_hidden": true,
"published_hidden": true,
"args": [
"example"
]
}
]
}
set account wording visibility
PATCH /design/wording/visibility
Hides or shows one line of the account's wording, where the line can be hidden, in the design draft. Publish the draft with POST /design/publish.
Requires settings.manage
Query parameters
localestring
Request body
keystring required
Response fields
localestringaudiencestring- The audience slug whose own wording
draftandpublishedcarry, or an empty string for everyone's wording. termsinteger- The id of the payment terms whose own wording
draftandpublishedcarry, or 0 for everyone's wording. Never set beside a non-emptyaudience. draft_openbooleanwordsarray of DesignWordingRowwords[].keystringwords[].screensarray of stringwords[].formsarray of stringwords[].hideablebooleanwords[].formattableboolean- The line may carry bold, italic, bullets and links. Once formatted, its draft and published values are a composer body rather than a plain sentence.
words[].per_audienceboolean- An audience of the event can have its own wording for this line, on top of everyone's. Only these lines are listed with
?audience=, and only these acceptaudiencein a change. words[].per_termsboolean- A set of payment terms can have its own wording for this line, on top of everyone's: the lines of the invoice emails. Only these lines are listed with
?terms=, and only these accepttermsin a change. No line is bothper_audienceandper_terms. words[].draftobjectwords[].publishedobjectwords[].argsarray of string
Responses
200The wording for this scope and locale.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422Invalid locale or visibility input.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/design/wording/visibility \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"key":"example","hidden":true}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/design/wording/visibility", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"key": "example",
"hidden": true
}),
});
const data = await res.json();
{
"locale": "example",
"audience": "example",
"terms": 1,
"draft_open": true,
"words": [
{
"key": "example",
"screens": [
"example"
],
"forms": [
"example"
],
"hideable": true,
"formattable": true,
"per_audience": true,
"per_terms": true,
"draft": {},
"published": {},
"draft_hidden": true,
"published_hidden": true,
"args": [
"example"
]
}
]
}
List the account's looks
GET /looks
The colour-and-template sets this account has made, and which of them it defaults to. A look is a live link. Editing one moves every event on it, which is why events is on every read rather than something a caller has to go and count — the organizer's own screens report it for the same reason, because a surface that did not say how many would let somebody find out afterwards.
The theme FILES are not served here: templates and assets are pushed with titogo push, which is a working copy of a directory rather than a shape a JSON body can carry. What this serves is the look as a RECORD — its name, its colours, and its standing. files reports how many it carries.
The manifests are not in the list — a list of those would be a list of documents. GET /looks/{id} carries one.
Requires settings.manage
Response fields
looksarray of Looklooks[].idintegerlooks[].namestring- What an organizer reads in the picker. At most 60 characters; duplicates are allowed.
looks[].has_coloursboolean- Whether it carries a manifest of its own. False is a real state — it may carry templates instead.
looks[].manifest_tomlstring- Its colours in the form a designer authors, re-encoded from what is stored. Absent from the list, and absent when it has none.
looks[].site_header.logo_urlstring- An absolute https:// URL, or a path on the account's own host starting with /.
looks[].site_header.logo_altstring- The logo's accessible name, or the words drawn in its place when there is no logo. Required with a logo_url.
looks[].site_header.links[].urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
looks[].site_header.action.urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
looks[].created_atstring (RFC 3339) or null
Responses
200The looks.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/looks \
-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/looks", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"looks": [
{
"id": 3,
"name": "Winter 2027",
"has_colours": true,
"files": 12,
"events": 4,
"account_default": false,
"manifest_toml": "[palette]\n background = \"#f6f6fb\"\n card = \"#ffffff\"\n text = \"#3c4257\"\n heading = \"#0a1f44\"\n link = \"#2d40ea\"\n button_bg = \"#2d40ea\"\n button_text = \"#ffffff\"\n\n[shape]\n radius = 0\n shadow = \"none\"\n buttons = \"rounded\"\n\n[type]\n body = \"inter\"\n heading = \"inter\"\n scale = \"regular\"\n [type.custom]\n family = \"\"\n apply = \"\"\n\n[motif]\n band = \"none\"\n section_wash = \"none\"\n wash_color = \"\"\n",
"site_header": {
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"home_url": "https://www.example.com/",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
],
"action": {
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
},
"site_footer": {
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"social": [
{
"kind": "instagram",
"url": "https://www.instagram.com/example"
}
],
"columns": [
{
"heading": "About us",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
]
}
],
"image_url": "/assets/wordmark.svg",
"image_alt": "Example Ltd",
"note": "© Example Ltd"
},
"created_at": "2026-09-08T15:20:00Z"
}
]
}
Make a look
POST /looks
One verb for both kinds, as the organizer's own screen has: a look carries colours, templates, or both, and which it happens to have is not a category anybody should pick before they have started. The manifest travels as TOML in both directions, because that is what a designer authors and what the CLI pushes. It is validated to a CLOSED SET before it is stored, since what comes out of it reaches CSS; the TOML answered back is re-encoded from the stored form, so it is canonical rather than whatever was pasted.
Two looks MAY share a name — the name is what an organizer reads in the picker, and refusing a duplicate would be a rule the admin's own form does not have.
Requires settings.manage
Request body
manifest_tomlstring- The colours, fonts, shape and motifs, as a TOML theme file. Every setting it can name, with the values each one takes, is listed at /docs/account/import-a-theme. A setting not on that list is refused (422
invalid_body), with the message naming it and the nearest real setting where one is close. "" clears them.
Response fields
idintegerhas_coloursboolean- Whether it carries a manifest of its own. False is a real state — it may carry templates instead.
manifest_tomlstring- Its colours in the form a designer authors, re-encoded from what is stored. Absent from the list, and absent when it has none.
site_header.logo_urlstring- An absolute https:// URL, or a path on the account's own host starting with /.
site_header.logo_altstring- The logo's accessible name, or the words drawn in its place when there is no logo. Required with a logo_url.
site_header.links[].urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
site_header.action.urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
created_atstring (RFC 3339) or null
Responses
201The look as made.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422A blank name, a name over 60 characters, or amanifest_tomlthat does not parse, names a setting it does not have, or names a value outside the closed set (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/looks \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Winter 2027"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/looks", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Winter 2027"
}),
});
const data = await res.json();
{
"id": 3,
"name": "Winter 2027",
"has_colours": true,
"files": 12,
"events": 4,
"account_default": false,
"manifest_toml": "[palette]\n background = \"#f6f6fb\"\n card = \"#ffffff\"\n text = \"#3c4257\"\n heading = \"#0a1f44\"\n link = \"#2d40ea\"\n button_bg = \"#2d40ea\"\n button_text = \"#ffffff\"\n\n[shape]\n radius = 0\n shadow = \"none\"\n buttons = \"rounded\"\n\n[type]\n body = \"inter\"\n heading = \"inter\"\n scale = \"regular\"\n [type.custom]\n family = \"\"\n apply = \"\"\n\n[motif]\n band = \"none\"\n section_wash = \"none\"\n wash_color = \"\"\n",
"site_header": {
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"home_url": "https://www.example.com/",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
],
"action": {
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
},
"site_footer": {
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"social": [
{
"kind": "instagram",
"url": "https://www.instagram.com/example"
}
],
"columns": [
{
"heading": "About us",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
]
}
],
"image_url": "/assets/wordmark.svg",
"image_alt": "Example Ltd",
"note": "© Example Ltd"
},
"created_at": "2026-09-08T15:20:00Z"
}
Delete a look
DELETE /looks/{id}
Every event on this look goes back to Tito's own page — the biggest look change there is. The organizer's own screen puts a confirmation in front of that which says how many events it is; a caller gets none, because a confirmation is a person being asked, so the ANSWER carries the number instead. It is the only way a script learns what it just did. Purges every public page.
Requires settings.manage
Path parameters
idinteger required- The look's id, as
GET /lookslists it.
Response fields
deletedboolean
Responses
200It is gone, and how many events moved.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/looks/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/looks/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"deleted": true,
"events_moved": 4
}
Get a look
GET /looks/{id}
One look, with its colours as TOML. A look is a live link. Editing one moves every event on it, which is why events is on every read rather than something a caller has to go and count — the organizer's own screens report it for the same reason, because a surface that did not say how many would let somebody find out afterwards.
Requires settings.manage
Path parameters
idinteger required- The look's id, as
GET /lookslists it.
Response fields
idintegerhas_coloursboolean- Whether it carries a manifest of its own. False is a real state — it may carry templates instead.
manifest_tomlstring- Its colours in the form a designer authors, re-encoded from what is stored. Absent from the list, and absent when it has none.
site_header.logo_urlstring- An absolute https:// URL, or a path on the account's own host starting with /.
site_header.logo_altstring- The logo's accessible name, or the words drawn in its place when there is no logo. Required with a logo_url.
site_header.links[].urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
site_header.action.urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
created_atstring (RFC 3339) or null
Responses
200The look.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/looks/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/looks/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"name": "Winter 2027",
"has_colours": true,
"files": 12,
"events": 4,
"account_default": false,
"manifest_toml": "[palette]\n background = \"#f6f6fb\"\n card = \"#ffffff\"\n text = \"#3c4257\"\n heading = \"#0a1f44\"\n link = \"#2d40ea\"\n button_bg = \"#2d40ea\"\n button_text = \"#ffffff\"\n\n[shape]\n radius = 0\n shadow = \"none\"\n buttons = \"rounded\"\n\n[type]\n body = \"inter\"\n heading = \"inter\"\n scale = \"regular\"\n [type.custom]\n family = \"\"\n apply = \"\"\n\n[motif]\n band = \"none\"\n section_wash = \"none\"\n wash_color = \"\"\n",
"site_header": {
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"home_url": "https://www.example.com/",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
],
"action": {
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
},
"site_footer": {
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"social": [
{
"kind": "instagram",
"url": "https://www.instagram.com/example"
}
],
"columns": [
{
"heading": "About us",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
]
}
],
"image_url": "/assets/wordmark.svg",
"image_alt": "Example Ltd",
"note": "© Example Ltd"
},
"created_at": "2026-09-08T15:20:00Z"
}
Rename a look, or change its colours
PATCH /looks/{id}
Only the keys present change. Naming manifest_toml as "" CLEARS the colours — that is the organizer's own Remove colours verb rather than a missing field, and a look with none is a real state.
Changing the colours purges every public page, because they reach every page on this look. A rename purges nothing: it changes nothing a buyer sees. The manifest travels as TOML in both directions, because that is what a designer authors and what the CLI pushes. It is validated to a CLOSED SET before it is stored, since what comes out of it reaches CSS; the TOML answered back is re-encoded from the stored form, so it is canonical rather than whatever was pasted.
Requires settings.manage
Path parameters
idinteger required- The look's id, as
GET /lookslists it.
Request body
manifest_tomlstring- The colours, fonts, shape and motifs, as a TOML theme file. Every setting it can name, with the values each one takes, is listed at /docs/account/import-a-theme. A setting not on that list is refused (422
invalid_body), with the message naming it and the nearest real setting where one is close. "" clears them.
Response fields
idintegerhas_coloursboolean- Whether it carries a manifest of its own. False is a real state — it may carry templates instead.
manifest_tomlstring- Its colours in the form a designer authors, re-encoded from what is stored. Absent from the list, and absent when it has none.
site_header.logo_urlstring- An absolute https:// URL, or a path on the account's own host starting with /.
site_header.logo_altstring- The logo's accessible name, or the words drawn in its place when there is no logo. Required with a logo_url.
site_header.links[].urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
site_header.action.urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
created_atstring (RFC 3339) or null
Responses
200The look as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422A blank name, a name over 60 characters, or amanifest_tomlthat does not parse, names a setting it does not have, or names a value outside the closed set (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/looks/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Winter 2027"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/looks/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Winter 2027"
}),
});
const data = await res.json();
{
"id": 3,
"name": "Winter 2027",
"has_colours": true,
"files": 12,
"events": 4,
"account_default": false,
"manifest_toml": "[palette]\n background = \"#f6f6fb\"\n card = \"#ffffff\"\n text = \"#3c4257\"\n heading = \"#0a1f44\"\n link = \"#2d40ea\"\n button_bg = \"#2d40ea\"\n button_text = \"#ffffff\"\n\n[shape]\n radius = 0\n shadow = \"none\"\n buttons = \"rounded\"\n\n[type]\n body = \"inter\"\n heading = \"inter\"\n scale = \"regular\"\n [type.custom]\n family = \"\"\n apply = \"\"\n\n[motif]\n band = \"none\"\n section_wash = \"none\"\n wash_color = \"\"\n",
"site_header": {
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"home_url": "https://www.example.com/",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
],
"action": {
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
},
"site_footer": {
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"social": [
{
"kind": "instagram",
"url": "https://www.instagram.com/example"
}
],
"columns": [
{
"heading": "About us",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
]
}
],
"image_url": "/assets/wordmark.svg",
"image_alt": "Example Ltd",
"note": "© Example Ltd"
},
"created_at": "2026-09-08T15:20:00Z"
}
List declared event fields
GET /looks/{id}/fields
Returns each field declared by the theme, with its key, label, kind, description, options, path, line, state, and field_id. The state is ok, missing, archived, or conflict. The response also includes parse warnings. Requires settings.manage. Look ID 0 selects legacy account files. Returns 404 for a missing or invalid look.
Requires settings.manage
Path parameters
idinteger required
Response fields
warningsarray of stringfieldsarray of ThemeFieldfields[].keystringfields[].labelstringfields[].kindstringfields[].descriptionstringfields[].pathstringfields[].statestringfields[].optionsarray of stringfields[].lineintegerfields[].field_idinteger
Responses
200List declared event fields401No 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/looks/1/fields \
-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/looks/1/fields", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"warnings": [
"example"
],
"fields": [
{
"key": "example",
"label": "example",
"kind": "ticket",
"description": "example",
"path": "example",
"state": "example",
"options": [
"example"
],
"line": 1,
"field_id": 1
}
]
}
Accept declared event fields
POST /looks/{id}/fields/accept
Accepts the theme's field declarations. Send no request body. The endpoint rereads the declarations, creates missing event fields, restores archived event fields, and skips conflicts with personal fields. It returns created, restored, and warnings. Existing keys stay unchanged. Repeated requests are safe, and all writes use one transaction. Requires settings.manage. Look ID 0 selects legacy account files. Returns 404 for a missing or invalid look.
Requires settings.manage
Path parameters
idinteger required
Response fields
Responses
200Accept declared event fields401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/looks/1/fields/accept \
-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/looks/1/fields/accept", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"warnings": [
"example"
],
"created": 1,
"restored": 1
}
Delete a theme file
DELETE /looks/{id}/file
Deletes the file selected by the path query parameter. Use 0 to select legacy account files. Positive IDs select an existing look. An unknown positive ID returns 404. The request requires settings.manage. Deletion affects every event using the look.
Requires settings.manage
Path parameters
idinteger required
Query parameters
pathstring required
Responses
204Delete a theme file401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid file500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/looks/1/file \
-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/looks/1/file", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
Get a theme file
GET /looks/{id}/file
Gets the file selected by the path query parameter. Use 0 to select legacy account files. Positive IDs select an existing look. An unknown positive ID returns 404. The request requires settings.manage. The response includes path, size, version, body or body_base64, and missing_purchase_form. An editor link is included when available. Binary files use body_base64.
Requires settings.manage
Path parameters
idinteger required
Query parameters
pathstring required
Response fields
pathstringsizeintegerversionstringbodystringbody_base64stringadmin_urlstringmissing_purchase_formboolean
Responses
200Get a theme file401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid file500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/looks/1/file \
-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/looks/1/file", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"path": "example",
"size": 1,
"version": "example",
"body": "example",
"body_base64": "example",
"admin_url": "https://example.com/hooks/tito",
"missing_purchase_form": true
}
Replace a theme file
PATCH /looks/{id}/file
Replaces the contents of the file selected by the path query parameter. Send exactly one of body for UTF-8 text content or body_base64 for standard base64 content. The decoded content can be up to 1 MiB. Paths cannot be changed by this request. Templates must compile before the file is saved. Send the current version in base when checking for concurrent edits. The server returns 409 if the base version is stale or another edit happens first. Use 0 for legacy account files or a positive ID for an existing look. An unknown positive ID returns 404. The request requires settings.manage. Edits apply immediately to every event using the look.
Requires settings.manage
Path parameters
idinteger required
Query parameters
pathstring required
Request body
Response fields
pathstringsizeintegerversionstringbodystringbody_base64stringadmin_urlstringmissing_purchase_formboolean
Responses
200Replace a theme file401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Conflict422Invalid file500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/looks/1/file \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"body":"example"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/looks/1/file", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"body": "example"
}),
});
const data = await res.json();
{
"path": "example",
"size": 1,
"version": "example",
"body": "example",
"body_base64": "example",
"admin_url": "https://example.com/hooks/tito",
"missing_purchase_form": true
}
List files in a look
GET /looks/{id}/files
Lists the stored files for a look without file contents. Use 0 to select legacy account files. Positive IDs select an existing look. An unknown positive ID returns 404. The request requires settings.manage. Each file includes path, size, and version. An editor link is included when available.
Requires settings.manage
Path parameters
idinteger required
Response fields
filesarray of ThemeFilefiles[].pathstringfiles[].sizeintegerfiles[].versionstringfiles[].bodystringfiles[].body_base64stringfiles[].admin_urlstringfiles[].missing_purchase_formboolean
Responses
200List files in a look401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid file500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/looks/1/files \
-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/looks/1/files", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"files": [
{
"path": "example",
"size": 1,
"version": "example",
"body": "example",
"body_base64": "example",
"admin_url": "https://example.com/hooks/tito",
"missing_purchase_form": true
}
]
}
Create a file in a look
POST /looks/{id}/files
Creates a file with the path in the request body. Send exactly one of body for UTF-8 text content or body_base64 for standard base64 content. The decoded content can be up to 1 MiB. Paths must meet the theme file path rules. Templates must compile before the file is saved. Binary files are returned in body_base64. Use 0 for legacy account files or a positive ID for an existing look. An unknown positive ID returns 404. The request requires settings.manage. Edits apply immediately to every event using the look.
Requires settings.manage
Path parameters
idinteger required
Request body
Response fields
pathstringsizeintegerversionstringbodystringbody_base64stringadmin_urlstringmissing_purchase_formboolean
Responses
201Create a file in a look401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Conflict422Invalid file500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/looks/1/files \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"path":"example"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/looks/1/files", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"path": "example"
}),
});
const data = await res.json();
{
"path": "example",
"size": 1,
"version": "example",
"body": "example",
"body_base64": "example",
"admin_url": "https://example.com/hooks/tito",
"missing_purchase_form": true
}
Make a look the account's default
POST /looks/{id}/select
Every event that has not chosen for itself moves to this look — that is what a look being a live link means, and it is why this purges every public page. An event that HAS chosen keeps its own.
Requires settings.manage
Path parameters
idinteger required- The look's id, as
GET /lookslists it.
Response fields
idintegerhas_coloursboolean- Whether it carries a manifest of its own. False is a real state — it may carry templates instead.
manifest_tomlstring- Its colours in the form a designer authors, re-encoded from what is stored. Absent from the list, and absent when it has none.
site_header.logo_urlstring- An absolute https:// URL, or a path on the account's own host starting with /.
site_header.logo_altstring- The logo's accessible name, or the words drawn in its place when there is no logo. Required with a logo_url.
site_header.links[].urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
site_header.action.urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
created_atstring (RFC 3339) or null
Responses
200The look, now the account's default.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/looks/1/select \
-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/looks/1/select", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"name": "Winter 2027",
"has_colours": true,
"files": 12,
"events": 4,
"account_default": false,
"manifest_toml": "[palette]\n background = \"#f6f6fb\"\n card = \"#ffffff\"\n text = \"#3c4257\"\n heading = \"#0a1f44\"\n link = \"#2d40ea\"\n button_bg = \"#2d40ea\"\n button_text = \"#ffffff\"\n\n[shape]\n radius = 0\n shadow = \"none\"\n buttons = \"rounded\"\n\n[type]\n body = \"inter\"\n heading = \"inter\"\n scale = \"regular\"\n [type.custom]\n family = \"\"\n apply = \"\"\n\n[motif]\n band = \"none\"\n section_wash = \"none\"\n wash_color = \"\"\n",
"site_header": {
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"home_url": "https://www.example.com/",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
],
"action": {
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
},
"site_footer": {
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"social": [
{
"kind": "instagram",
"url": "https://www.instagram.com/example"
}
],
"columns": [
{
"heading": "About us",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
]
}
],
"image_url": "/assets/wordmark.svg",
"image_alt": "Example Ltd",
"note": "© Example Ltd"
},
"created_at": "2026-09-08T15:20:00Z"
}
Checkout
Get the checkout settings
GET /checkout
The account's defaults for what a buyer sees and is asked at checkout: whether public prices are advertised with tax in them, whether a buyer can say they are booking for a company, and whether the checkout asks for e-invoicing identifiers. An event can give its own answer to the first two (GET /events/{slug}/settings/checkout); the account's answer applies to every event that has not.
Requires settings.manage
Response fields
tax_display_excludesboolean- Public prices are advertised exclusive of tax, on every event without its own answer.
ask_companyboolean- Whether the checkout asks a buyer if they are buying for a company, on every event without its own answer. On for every account that has not turned it off. A buyer who ticks it gives a company name, country and VAT number, and those become the party the invoice is addressed to (see
companyon an order). sends_einvoicesboolean- Whether the checkout also asks for the country-specific identifier an e-invoice needs, on top of the VAT number. OFF for every account that has not turned it on, which is the opposite default to
ask_companyand deliberate: almost no organizer files structured e-invoices. Three countries have such an identifier (Italy, France, Germany) and every other country routes on the VAT number, so for most buyers nothing extra is asked even with this on. Does nothing whileask_companyis off. Account-wide: events have no override. collect_unfinished_new_eventsboolean- Whether a new event starts with
collect_unfinishedswitched on in its own Checkout settings. Moves no event that already exists. Off for every account that has not turned it on.
Responses
200The checkout settings.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/checkout \
-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/checkout", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"tax_display_excludes": false,
"ask_company": true,
"sends_einvoices": false,
"collect_unfinished_new_events": false
}
Change the checkout settings
PATCH /checkout
Only the keys present change. tax_display_excludes decides the price every public event page ADVERTISES, so moving it purges every public page; setting it to what it already is purges nothing. These three fields are also still accepted by PATCH /invoicing, where they lived before this panel existed.
Requires settings.manage
Request body
tax_display_excludesboolean- Public prices are advertised exclusive of tax, on every event without its own answer.
ask_companyboolean- Whether the checkout asks a buyer if they are buying for a company, on every event without its own answer. On for every account that has not turned it off. A buyer who ticks it gives a company name, country and VAT number, and those become the party the invoice is addressed to (see
companyon an order). sends_einvoicesboolean- Whether the checkout also asks for the country-specific identifier an e-invoice needs, on top of the VAT number. OFF for every account that has not turned it on, which is the opposite default to
ask_companyand deliberate: almost no organizer files structured e-invoices. Three countries have such an identifier (Italy, France, Germany) and every other country routes on the VAT number, so for most buyers nothing extra is asked even with this on. Does nothing whileask_companyis off. Account-wide: events have no override. collect_unfinished_new_eventsboolean- Whether a new event starts with
collect_unfinishedswitched on in its own Checkout settings. Moves no event that already exists. Off for every account that has not turned it on.
Response fields
tax_display_excludesboolean- Public prices are advertised exclusive of tax, on every event without its own answer.
ask_companyboolean- Whether the checkout asks a buyer if they are buying for a company, on every event without its own answer. On for every account that has not turned it off. A buyer who ticks it gives a company name, country and VAT number, and those become the party the invoice is addressed to (see
companyon an order). sends_einvoicesboolean- Whether the checkout also asks for the country-specific identifier an e-invoice needs, on top of the VAT number. OFF for every account that has not turned it on, which is the opposite default to
ask_companyand deliberate: almost no organizer files structured e-invoices. Three countries have such an identifier (Italy, France, Germany) and every other country routes on the VAT number, so for most buyers nothing extra is asked even with this on. Does nothing whileask_companyis off. Account-wide: events have no override. collect_unfinished_new_eventsboolean- Whether a new event starts with
collect_unfinishedswitched on in its own Checkout settings. Moves no event that already exists. Off for every account that has not turned it on.
Responses
200The checkout settings as they now stand.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 held a value this panel does not know (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/checkout \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"tax_display_excludes":false}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/checkout", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"tax_display_excludes": false
}),
});
const data = await res.json();
{
"tax_display_excludes": false,
"ask_company": true,
"sends_einvoices": false,
"collect_unfinished_new_events": false
}
Taxes
List the tax rates
GET /tax-rates
A tax rate is an ACCOUNT record — every event's pickers offer the same library — so these live at the account level even though the admin reaches the same screen from an event's settings and from the account's. A percentage is set once, when the rate is created: changing it would silently change what every ticket type pointing at it charges from then on, and the way to charge a different percentage is a different rate. Live rates first, each with how many products point at it.
Requires settings.manage
Response fields
tax_ratesarray of TaxRatetax_rates[].idintegertax_rates[].namestringtax_rates[].rate_milli_pctinteger- The percentage in thousandths of a percent: 23000 is 23%, 9975 is 9.975%. Set once, at creation.
tax_rates[].rate_bpsinteger- The same percentage in basis points, 2300 for 23%, rounded half up where the rate has a third decimal (9.975% is 998). Charge by
rate_milli_pct. tax_rates[].vat_categorystring- The EN 16931 code documents declare for lines carrying this rate; "" is "not stated".
tax_rates[].used_byinteger- How many products point at the rate. One nothing points at can be deleted.
Responses
200The library.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/tax-rates \
-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/tax-rates", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"tax_rates": [
{
"id": 3,
"name": "VAT",
"jurisdiction": "Ireland",
"rate_milli_pct": 23000,
"rate_bps": 2300,
"vat_category": "S",
"exemption_reason": "",
"used_by": 0,
"archived": false
}
]
}
Add a tax rate
POST /tax-rates
Mints a rate. The duplicate check runs here as it does on every surface that mints one — a check on a single form would leave another free to make the twin it just refused.
Requires settings.manage
Request body
namestringjurisdictionstringrate_milli_pctinteger- Create only: 0 to 100000 thousandths of a percent, up to three decimals (9975 is 9.975%).
rate_bpsinteger- Create only: 0 to 10000 basis points. Sent with
rate_milli_pct, it must be that rate rounded half up. exemption_reasonstring- Required by every category except S and Z; ignored where the category needs none.
Response fields
idintegernamestringrate_milli_pctinteger- The percentage in thousandths of a percent: 23000 is 23%, 9975 is 9.975%. Set once, at creation.
rate_bpsinteger- The same percentage in basis points, 2300 for 23%, rounded half up where the rate has a third decimal (9.975% is 998). Charge by
rate_milli_pct. vat_categorystring- The EN 16931 code documents declare for lines carrying this rate; "" is "not stated".
Responses
201The new rate.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409A rate with that name, place and percentage already exists (duplicate); the message says whether it is archived, because only then is there one to restore.422The body was unreadable or carried an unknown field, the name or jurisdiction was blank, the rate was absent, outside 0% to 100%, finer than three decimals, or sent both ways and disagreeing, or the VAT category does not go with the rate (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/tax-rates \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"VAT"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/tax-rates", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "VAT"
}),
});
const data = await res.json();
{
"id": 3,
"name": "VAT",
"jurisdiction": "Ireland",
"rate_milli_pct": 23000,
"rate_bps": 2300,
"vat_category": "S",
"exemption_reason": "",
"used_by": 0,
"archived": false
}
Remove a tax rate
DELETE /tax-rates/{id}
One verb doing one of two things, as the admin's own control does: a rate nothing points at is DELETED, a rate in use is ARCHIVED instead — a rate in use must never silently stop taxing. The decision is made from the database rather than from what the caller says, so a rate something came to point at meanwhile cannot be destroyed. A delete answers {id, deleted: true}; an archive answers the rate, with archived true.
Requires settings.manage
Path parameters
idinteger required- The rate's id, as
GET /tax-rateslists it.
Responses
200Deleted, or archived because something points at it.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/tax-rates/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/tax-rates/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
Get a tax rate
GET /tax-rates/{id}
One rate, with its usage count.
Requires settings.manage
Path parameters
idinteger required- The rate's id, as
GET /tax-rateslists it.
Response fields
idintegernamestringrate_milli_pctinteger- The percentage in thousandths of a percent: 23000 is 23%, 9975 is 9.975%. Set once, at creation.
rate_bpsinteger- The same percentage in basis points, 2300 for 23%, rounded half up where the rate has a third decimal (9.975% is 998). Charge by
rate_milli_pct. vat_categorystring- The EN 16931 code documents declare for lines carrying this rate; "" is "not stated".
Responses
200The rate.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/tax-rates/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/tax-rates/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"name": "VAT",
"jurisdiction": "Ireland",
"rate_milli_pct": 23000,
"rate_bps": 2300,
"vat_category": "S",
"exemption_reason": "",
"used_by": 0,
"archived": false
}
Update a tax rate
PATCH /tax-rates/{id}
The name, where it applies, and what it declares on a document. A rate, as rate_milli_pct or rate_bps, is REFUSED: a percentage is set once, at creation. Renaming can walk a rate onto a twin of another just as adding can, so the duplicate check runs here too — against the other rates, since a rate is not its own duplicate.
Requires settings.manage
Path parameters
idinteger required- The rate's id, as
GET /tax-rateslists it.
Request body
namestringjurisdictionstringrate_milli_pctinteger- Create only: 0 to 100000 thousandths of a percent, up to three decimals (9975 is 9.975%).
rate_bpsinteger- Create only: 0 to 10000 basis points. Sent with
rate_milli_pct, it must be that rate rounded half up. exemption_reasonstring- Required by every category except S and Z; ignored where the category needs none.
Response fields
idintegernamestringrate_milli_pctinteger- The percentage in thousandths of a percent: 23000 is 23%, 9975 is 9.975%. Set once, at creation.
rate_bpsinteger- The same percentage in basis points, 2300 for 23%, rounded half up where the rate has a third decimal (9.975% is 998). Charge by
rate_milli_pct. vat_categorystring- The EN 16931 code documents declare for lines carrying this rate; "" is "not stated".
Responses
200The rate as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409A rate with that name, place and percentage already exists (duplicate); the message says whether it is archived, because only then is there one to restore.422A rate was sent, the name or jurisdiction was blanked, or the VAT category does not go with the rate (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/tax-rates/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"VAT"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/tax-rates/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "VAT"
}),
});
const data = await res.json();
{
"id": 3,
"name": "VAT",
"jurisdiction": "Ireland",
"rate_milli_pct": 23000,
"rate_bps": 2300,
"vat_category": "S",
"exemption_reason": "",
"used_by": 0,
"archived": false
}
Archive a tax rate
POST /tax-rates/{id}/archive
Takes the rate out of every picker. Products pointing at it keep taxing — archiving must never silently zero anyone's tax — so this is reversible housekeeping, not a delete.
Requires settings.manage
Path parameters
idinteger required- The rate's id, as
GET /tax-rateslists it.
Response fields
idintegernamestringrate_milli_pctinteger- The percentage in thousandths of a percent: 23000 is 23%, 9975 is 9.975%. Set once, at creation.
rate_bpsinteger- The same percentage in basis points, 2300 for 23%, rounded half up where the rate has a third decimal (9.975% is 998). Charge by
rate_milli_pct. vat_categorystring- The EN 16931 code documents declare for lines carrying this rate; "" is "not stated".
Responses
200The rate as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/tax-rates/1/archive \
-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/tax-rates/1/archive", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"name": "VAT",
"jurisdiction": "Ireland",
"rate_milli_pct": 23000,
"rate_bps": 2300,
"vat_category": "S",
"exemption_reason": "",
"used_by": 0,
"archived": false
}
Restore a tax rate
POST /tax-rates/{id}/restore
Puts an archived rate back in the pickers.
Requires settings.manage
Path parameters
idinteger required- The rate's id, as
GET /tax-rateslists it.
Response fields
idintegernamestringrate_milli_pctinteger- The percentage in thousandths of a percent: 23000 is 23%, 9975 is 9.975%. Set once, at creation.
rate_bpsinteger- The same percentage in basis points, 2300 for 23%, rounded half up where the rate has a third decimal (9.975% is 998). Charge by
rate_milli_pct. vat_categorystring- The EN 16931 code documents declare for lines carrying this rate; "" is "not stated".
Responses
200The rate as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/tax-rates/1/restore \
-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/tax-rates/1/restore", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"name": "VAT",
"jurisdiction": "Ireland",
"rate_milli_pct": 23000,
"rate_bps": 2300,
"vat_category": "S",
"exemption_reason": "",
"used_by": 0,
"archived": false
}
Invoicing
Get the invoicing settings
GET /invoicing
The account's invoicing set-up: who the seller is on an invoice, and where a bank transfer goes. tax_display_excludes, ask_company and sends_einvoices are still served here, where they lived before GET /checkout existed, and read the same values. The DOCUMENT NUMBERING half of the same screen is not served here and is recorded as the coverage gap: a series that has already issued numbers is locked, and "what a caller may still change about a series in flight" is a design question rather than a field list.
Requires settings.manage
Response fields
seller_address.line1stringseller_address.line2stringseller_address.citystringseller_address.post_codestringseller_address.countrystring- The seller's own 2-letter code, which decides whether the VAT question applies at all.
vat_statusstring- "" is UNSTATED — the default, meaning nobody has answered, never "not registered". An EU seller cannot save while it is unstated.
- one of
registerednot_registered bank_account_holderstring- The account's own bank account, stored exactly as typed — an IBAN is validated by the bank that receives the wire, and a pattern we get wrong is a sale refused.
bank_ibanstring- Holder and IBAN both being filled in is what decides whether buyers are offered bank transfer at all.
bank_bicstringbank_transfer_railstringmanualis the account's own account, reconciled by hand;stripeis a virtual IBAN per buyer.- one of
manualstripe bank_transfer_rail_choice_liveboolean- Whether the choice is live for this account at all: it needs complete bank details, a Stripe connection, and at least one euro event taking transfers. Where this is false the stored rail is not what a buyer would meet.
tax_display_excludesboolean- Public prices are advertised exclusive of tax. The same value
GET /checkoutserves. ask_companyboolean- Whether the checkout asks a buyer if they are buying for a company. On for every account that has not turned it off. A buyer who ticks it gives a company name, country and VAT number, and those become the party the invoice is addressed to (see
companyon an order). Turned off, the question is not on the checkout at all and the fields cannot be posted. sends_einvoicesboolean- Whether the checkout also asks for the country-specific identifier an e-invoice needs, on top of the VAT number. OFF for every account that has not turned it on, which is the opposite default to
ask_companyand deliberate: almost no organizer files structured e-invoices. Three countries have such an identifier (Italy, France, Germany) and every other country routes on the VAT number, so for most buyers nothing extra is asked even with this on. Does nothing whileask_companyis off. vendorobject- The vendor form's company block, by its own field keys (
seller_ein,seller_lei,seller_directors, …, andseller_signatory, the name printed under the signature). On a PATCH only the keys named change, and a key the block does not have is refused. vendor_signatureboolean- Whether vendor forms carry the account's signature, uploaded as an image in Settings, Invoicing. The name printed under it is
seller_signatoryinvendor. Every vendor form is dated the day it is downloaded.
Responses
200The invoicing settings.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/invoicing \
-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/invoicing", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"legal_name": "The Sea Church Ltd",
"address_text": "12 Quay St, Dungarvan",
"seller_address": {
"line1": "12 Quay Street",
"line2": "",
"city": "Dungarvan",
"post_code": "X35 YH61",
"subdivision": "Co. Waterford",
"country": "IE"
},
"legal_reg_id": "IE123456",
"vat_status": "registered",
"vat_number": "IE9825613N",
"bank_account_holder": "The Sea Church Ltd",
"bank_iban": "IE29 AIBK 9311 5212 3456 78",
"bank_bic": "AIBKIE2D",
"bank_country": "IE",
"bank_transfer_rail": "manual",
"bank_transfer_rail_choice_live": false,
"tax_display_excludes": false,
"ask_company": true,
"sends_einvoices": true,
"vendor": {
"seller_business_purpose": "Ticketing",
"seller_ein": "12-3456789"
},
"vendor_signature": true
}
Change the invoicing settings
PATCH /invoicing
Only the keys present change — ABSENT IS NOT BLANK. That rule is the important one here: reading a missing field as an empty one would blank a value nobody touched, and for bank_country it would silently move the country printed on every invoice issued afterwards. A stated "" is still a real answer, and for bank_country it means "read it off my IBAN".
The SELLER IDENTITY (seller_address, legal_reg_id, vat_status, vat_number) is validated as ONE THING and written all-or-nothing, so a refusal leaves the account exactly as it was rather than storing the half that passed. An EU seller cannot save while vat_status is unstated — unstated means nobody has answered, never "not registered" — and declaring not_registered clears any VAT number, because an invoice carrying a number its seller is not registered for is the worse of the two failures.
bank_transfer_rail decides where buyers' money is SENT, so a value that is neither manual nor stripe is refused rather than read as either. bank_transfer_rail_choice_live on the read says whether the choice is live for this account at all.
tax_display_excludes decides the price every public event page ADVERTISES, so moving it purges every public page; setting it to what it already is purges nothing. The DOCUMENT NUMBERING half of the same screen is not served here and is recorded as the coverage gap: a series that has already issued numbers is locked, and "what a caller may still change about a series in flight" is a design question rather than a field list.
Requires settings.manage
Request body
legal_namestringaddress_textstringseller_address.line1stringseller_address.line2stringseller_address.citystringseller_address.post_codestringseller_address.countrystring- The seller's own 2-letter code, which decides whether the VAT question applies at all.
legal_reg_idstringbank_account_holderstringbank_ibanstringbank_bicstringask_companyboolean- Whether the checkout asks a buyer if they are buying for a company. On for every account that has not turned it off. A buyer who ticks it gives a company name, country and VAT number, and those become the party the invoice is addressed to (see
companyon an order). Turned off, the question is not on the checkout at all and the fields cannot be posted. sends_einvoicesboolean- Whether the checkout also asks for the country-specific identifier an e-invoice needs, on top of the VAT number. OFF for every account that has not turned it on, which is the opposite default to
ask_companyand deliberate: almost no organizer files structured e-invoices. Three countries have such an identifier (Italy, France, Germany) and every other country routes on the VAT number, so for most buyers nothing extra is asked even with this on. Does nothing whileask_companyis off. vendorobject- The vendor form's company block, by its own field keys (
seller_ein,seller_lei,seller_directors, …, andseller_signatory, the name printed under the signature). On a PATCH only the keys named change, and a key the block does not have is refused.
Response fields
seller_address.line1stringseller_address.line2stringseller_address.citystringseller_address.post_codestringseller_address.countrystring- The seller's own 2-letter code, which decides whether the VAT question applies at all.
vat_statusstring- "" is UNSTATED — the default, meaning nobody has answered, never "not registered". An EU seller cannot save while it is unstated.
- one of
registerednot_registered bank_account_holderstring- The account's own bank account, stored exactly as typed — an IBAN is validated by the bank that receives the wire, and a pattern we get wrong is a sale refused.
bank_ibanstring- Holder and IBAN both being filled in is what decides whether buyers are offered bank transfer at all.
bank_bicstringbank_transfer_railstringmanualis the account's own account, reconciled by hand;stripeis a virtual IBAN per buyer.- one of
manualstripe bank_transfer_rail_choice_liveboolean- Whether the choice is live for this account at all: it needs complete bank details, a Stripe connection, and at least one euro event taking transfers. Where this is false the stored rail is not what a buyer would meet.
tax_display_excludesboolean- Public prices are advertised exclusive of tax. The same value
GET /checkoutserves. ask_companyboolean- Whether the checkout asks a buyer if they are buying for a company. On for every account that has not turned it off. A buyer who ticks it gives a company name, country and VAT number, and those become the party the invoice is addressed to (see
companyon an order). Turned off, the question is not on the checkout at all and the fields cannot be posted. sends_einvoicesboolean- Whether the checkout also asks for the country-specific identifier an e-invoice needs, on top of the VAT number. OFF for every account that has not turned it on, which is the opposite default to
ask_companyand deliberate: almost no organizer files structured e-invoices. Three countries have such an identifier (Italy, France, Germany) and every other country routes on the VAT number, so for most buyers nothing extra is asked even with this on. Does nothing whileask_companyis off. vendorobject- The vendor form's company block, by its own field keys (
seller_ein,seller_lei,seller_directors, …, andseller_signatory, the name printed under the signature). On a PATCH only the keys named change, and a key the block does not have is refused. vendor_signatureboolean- Whether vendor forms carry the account's signature, uploaded as an image in Settings, Invoicing. The name printed under it is
seller_signatoryinvendor. Every vendor form is dated the day it is downloaded.
Responses
200The settings, in the shapeGET /invoicinganswers with.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The seller identity was refused (an EU seller with no VAT position, a VAT status that is neither word, a registered seller with no number, a country that is not two letters); orbank_transfer_railwas notmanualorstripe; orvendornamed a field the company block does not have; or an explicit null or unknown field (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/invoicing \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"legal_name":"The Sea Church Ltd"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/invoicing", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"legal_name": "The Sea Church Ltd"
}),
});
const data = await res.json();
{
"legal_name": "The Sea Church Ltd",
"address_text": "12 Quay St, Dungarvan",
"seller_address": {
"line1": "12 Quay Street",
"line2": "",
"city": "Dungarvan",
"post_code": "X35 YH61",
"subdivision": "Co. Waterford",
"country": "IE"
},
"legal_reg_id": "IE123456",
"vat_status": "registered",
"vat_number": "IE9825613N",
"bank_account_holder": "The Sea Church Ltd",
"bank_iban": "IE29 AIBK 9311 5212 3456 78",
"bank_bic": "AIBKIE2D",
"bank_country": "IE",
"bank_transfer_rail": "manual",
"bank_transfer_rail_choice_live": false,
"tax_display_excludes": false,
"ask_company": true,
"sends_einvoices": true,
"vendor": {
"seller_business_purpose": "Ticketing",
"seller_ein": "12-3456789"
},
"vendor_signature": true
}
The document numbering series
GET /invoicing/numbering
The other half of the invoicing panel: what each document is called, whether the scope is still changeable, and what the next number of each kind would be.
The scope locks once a number has been issued. Going from account-wide to per-event after INV-00001..50 have been printed restarts every event at 1 and hands out numbers that are already on invoices somebody has filed, and "never reused" is the one property here that carries real compliance weight. scope_locked is a 409 rather than a 422 for that reason: the request is not malformed, the account has moved past the point where it could be granted.
Requires settings.manage
Response fields
scopestringaccount(the default, and what a tax authority generally expects — an event is a Tito concept, not a legal one) orevent.issued_countinteger- How many numbers this account has printed — the REASON the scope is fixed. "240 numbers have been issued" is a reason; "this is fixed" is an assertion.
zero_valuestring- What a zero-value order gets:
number(the default — a free registration is a real registration),unnumbered(a real document identified by its order ref, which spends no number and so locks nothing) ornone. kindsarray of DocumentNumberingKindkinds[].prefixstring- The TEMPLATE, not the rendered prefix: it may carry
{YYYY},{YY},{MM}and{EVENT}, over a deliberately narrow alphabet (letters, digits and- _ / . #and a space). This string is printed on an accounting document and typed into other people's systems. kinds[].resetstringnever, orannualfor a counter that restarts each January. An annual reset needs the year in the prefix or the suffix.kinds[].suffixstring- What comes after the count, a template like
prefixwith the same placeholders and alphabet. It starts with a letter or a mark, never a digit or a placeholder, so it can't run into the count.""is no suffix. kinds[].nextstring- What the next number of this kind WOULD look like. Reading it allocates nothing. Counted against the account's newest event, which is what a per-event series means anything against.
Responses
200The series.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/invoicing/numbering \
-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/invoicing/numbering", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"scope": "account",
"scope_locked": true,
"issued_count": 240,
"zero_value": "number",
"kinds": [
{
"kind": "invoice",
"prefix": "INV-{YYYY}-",
"pad": 5,
"reset": "annual",
"suffix": "-{YY}",
"next": "INV-2026-00043"
}
]
}
Change the numbering series
PATCH /invoicing/numbering
Validated and stored ALL AT ONCE: a refusal on one kind stores nothing at all, so a caller is never left working out which half took.
The scope locks once a number has been issued. Going from account-wide to per-event after INV-00001..50 have been printed restarts every event at 1 and hands out numbers that are already on invoices somebody has filed, and "never reused" is the one property here that carries real compliance weight. scope_locked is a 409 rather than a 422 for that reason: the request is not malformed, the account has moved past the point where it could be granted.
A setting that would wedge the series is refused rather than quietly accepted. An annual reset needs {YYYY} or {YY} in the prefix, because a counter that restarts in January without the year in the number regenerates last year's numbers, the unique index refuses them, and that kind can never be issued again. A per-event scope needs {EVENT} in EVERY prefix, for the same reason across two events rather than two years — which is why a request naming only scope is still checked against the prefixes it is not changing.
The readers degrade instead of wedging (an annual reset with no year reads as never), so an account can never be stuck; this refusal exists so that a door never silently disagrees with what somebody asked for.
Requires settings.manage
Request body
Response fields
scopestringaccount(the default, and what a tax authority generally expects — an event is a Tito concept, not a legal one) orevent.issued_countinteger- How many numbers this account has printed — the REASON the scope is fixed. "240 numbers have been issued" is a reason; "this is fixed" is an assertion.
zero_valuestring- What a zero-value order gets:
number(the default — a free registration is a real registration),unnumbered(a real document identified by its order ref, which spends no number and so locks nothing) ornone. kindsarray of DocumentNumberingKindkinds[].prefixstring- The TEMPLATE, not the rendered prefix: it may carry
{YYYY},{YY},{MM}and{EVENT}, over a deliberately narrow alphabet (letters, digits and- _ / . #and a space). This string is printed on an accounting document and typed into other people's systems. kinds[].resetstringnever, orannualfor a counter that restarts each January. An annual reset needs the year in the prefix or the suffix.kinds[].suffixstring- What comes after the count, a template like
prefixwith the same placeholders and alphabet. It starts with a letter or a mark, never a digit or a placeholder, so it can't run into the count.""is no suffix. kinds[].nextstring- What the next number of this kind WOULD look like. Reading it allocates nothing. Counted against the account's newest event, which is what a per-event series means anything against.
Responses
200The series as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.409Numbers have been issued, so the scope can no longer be changed (scope_locked).422A prefix, padding, reset rule, scope or zero-value policy this cannot store, or a kind that does not exist (invalid_body). The message says which series and why.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/invoicing/numbering \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"scope":"event"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/invoicing/numbering", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"scope": "event"
}),
});
const data = await res.json();
{
"scope": "account",
"scope_locked": true,
"issued_count": 240,
"zero_value": "number",
"kinds": [
{
"kind": "invoice",
"prefix": "INV-{YYYY}-",
"pad": 5,
"reset": "annual",
"suffix": "-{YY}",
"next": "INV-2026-00043"
}
]
}
Email senders
Remove the custom sending address
DELETE /email-sender
Takes the identity down at the mail service and goes back to the platform's address. Nothing is created half-way. The call out to the mail service IS the creation, so the row is only written once that call succeeds: a refusal or an unreachable service leaves an existing sender exactly as it was, and leaves no sender at all the first time. Removing runs the same order in reverse — a sender dropped before a failed call would leave the account unable to see or retry the one it still has.
Requires settings.manage
Response fields
idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
domainstringfrom_namestring- The display name. At most 100 characters, and never containing a control character.
statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive livebooleanstatus_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. records[].namestringrecords[].valuestringrecords[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. last_checkedstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladdressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. addresses[].smtp.portintegeraddresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused addresses[].created_atstring (RFC 3339) or null
Responses
200The sender, now gone; mail goes out from the platform's address.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409This account has no sender of its own (no_sender).500Internal error.502The mail service could not be reached; the sender is unchanged (relay_unavailable).
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/email-sender \
-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/email-sender", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
Get the default sending address
GET /email-sender
The account's own verified domain that ticket and order mail goes out from, or the platform's address when there is none.
available says whether this instance can have one AT ALL — it needs a relay to reach the mail service through. False means the feature is absent here, not that nobody has set it up.
Setting a sender up hands back the DNS records to publish — three DKIM CNAMEs, the MAIL FROM subdomain's MX and an SPF TXT — and that is the point of the answer rather than a detail of it. This is the endpoint where a machine is better placed than a person: a script driving Route 53 or Cloudflare can publish them itself, where an organizer copies five rows by hand.
The records are NOT a credential. A DKIM token's whole purpose is to sit in public DNS where anybody can read it; the SES credentials live on the relay and never reach an instance at all.
Requires settings.manage
Response fields
idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
domainstringfrom_namestring- The display name. At most 100 characters, and never containing a control character.
statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive livebooleanstatus_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. records[].namestringrecords[].valuestringrecords[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. last_checkedstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladdressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. addresses[].smtp.portintegeraddresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused addresses[].created_atstring (RFC 3339) or null
Responses
200The sender.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/email-sender \
-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/email-sender", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
Set up the default sending address
PUT /email-sender
Sets the sender up, or re-verifies it on a new domain. A PUT rather than a PATCH because all three fields are required — an address needs a domain, a local part and a display name — so there is no half a sender to merge into.
Nothing is created half-way. The call out to the mail service IS the creation, so the row is only written once that call succeeds: a refusal or an unreachable service leaves an existing sender exactly as it was, and leaves no sender at all the first time. Removing runs the same order in reverse — a sender dropped before a failed call would leave the account unable to see or retry the one it still has.
Setting a sender up hands back the DNS records to publish — three DKIM CNAMEs, the MAIL FROM subdomain's MX and an SPF TXT — and that is the point of the answer rather than a detail of it. This is the endpoint where a machine is better placed than a person: a script driving Route 53 or Cloudflare can publish them itself, where an organizer copies five rows by hand.
The records are NOT a credential. A DKIM token's whole purpose is to sit in public DNS where anybody can read it; the SES credentials live on the relay and never reach an instance at all.
Every bad field is reported at once, in the body's own names, so fixing two of them takes one round trip rather than two. A display name carrying a control character is REFUSED rather than quietly stripped: that is exactly what could plant a header on every future send.
Requires settings.manage
Request body
reply_tostring- Optional: one plain address replies to this address go to. Empty or absent sends replies to the From address.
Response fields
idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
domainstringfrom_namestring- The display name. At most 100 characters, and never containing a control character.
statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive livebooleanstatus_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. records[].namestringrecords[].valuestringrecords[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. last_checkedstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladdressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. addresses[].smtp.portintegeraddresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused addresses[].created_atstring (RFC 3339) or null
Responses
200The sender, pending verification, with the records to publish.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409This instance has no relay to reach the mail service through (unavailable), or the domain already sends from another Tito account, or is verified with the mail service by somebody else (domain_claimed). Neither changes on a retry; Tito support can move a sender between accounts.422domainis not a domain,from_localis not a local part,from_namecarries a control character, or the body had an explicit null or unknown field (invalid_body). The message names the fields.500Internal error.502The mail service could not be reached; nothing was changed (relay_unavailable).
curl -X PUT https://<yourslug>.go.tito.io/api/v1/email-sender \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"domain":"tickets.example.com","from_local":"tickets","from_name":"The Summit"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/email-sender", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit"
}),
});
const data = await res.json();
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
Ask where verification stands
POST /email-sender/check
Asks the mail service where DNS verification has got to and applies the answer.
This is the only way a sender becomes live, and a caller cannot ask for live. live is written only when everything is verified at once — DKIM, MAIL FROM and the identity itself — so there is no partial-live state and no writable status field. Short of that, status_note says which rung is missing.
A check that could not COMPLETE leaves the sender exactly as it was: an error is not evidence that a sender is gone, in either direction.
Requires settings.manage
Response fields
idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
domainstringfrom_namestring- The display name. At most 100 characters, and never containing a control character.
statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive livebooleanstatus_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. records[].namestringrecords[].valuestringrecords[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. last_checkedstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladdressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. addresses[].smtp.portintegeraddresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused addresses[].created_atstring (RFC 3339) or null
Responses
200The sender as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409This account has no sender of its own to check (no_sender).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/email-sender/check \
-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/email-sender/check", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
List the sending addresses
GET /email-senders
Every verified domain the account can send from, the default first. Each has any number of From addresses (addresses). An event sends as the address its email_sender_address_id names (GET /events/{slug}/settings/after-registering), or as the default address when that is 0. With no default, mail goes out from default_from.
A sender that isn't live yet sends nothing: an event that chose it sends from the default until it is.
Requires settings.manage
Response fields
availableboolean- Whether this instance can have senders at all. False means the feature is absent here.
default_fromstring- What mail goes out from when neither the event nor the account has a sender of its own.
senders[].idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. senders[].is_defaultboolean- True for the sender every event whose
email_sender_idis 0 sends from. senders[].configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
senders[].availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
senders[].domainstringsenders[].from_namestring- The display name. At most 100 characters, and never containing a control character.
senders[].statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive senders[].livebooleansenders[].status_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. senders[].not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
senders[].not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
senders[].modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp senders[].recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. senders[].records[].namestringsenders[].records[].valuestringsenders[].records[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. senders[].last_checkedstring (RFC 3339) or nullsenders[].created_atstring (RFC 3339) or nullsenders[].addressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. senders[].addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. senders[].addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. senders[].addresses[].smtp.portintegersenders[].addresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls senders[].addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. senders[].addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
senders[].addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
senders[].addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused senders[].addresses[].created_atstring (RFC 3339) or null
Responses
200Every sender.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/email-senders \
-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/email-senders", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"available": true,
"default_from": "tickets@ti.to",
"senders": [
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
]
}
Add a sending address
POST /email-senders
Sets a sender up on a domain and answers the DNS records to publish. On a domain the account already sends from, that sender is set up again in place (same id, so every event that chose it keeps it) and the address is added to it, or renamed when it has it already; its other addresses stay. The account's first sender becomes its default; a later one leaves the default alone (POST /email-senders/{id}/default changes it).
Nothing is created half-way: the call out to the mail service IS the creation, so a refusal or an unreachable service leaves every sender exactly as it was.
With smtp the sender sends through the account's own mail server instead: Tito signs in to it first, and nothing is saved unless that works. There are no records to publish, and the sender is live straight away. A domain sends one way or the other; asking for the other way on a domain the account already has answers 409 sender_mode. A server that refuses the sign-in answers 422 smtp_refused, naming the field it is about (smtp.password for a refused sign-in, smtp.host for anything about reaching the server) and the server's own words.
Requires settings.manage
Request body
domainstring required- The domain to send from. Without
smtp, you must be able to publish DNS records on it. reply_tostring- Optional: one plain address replies to this address go to. Empty or absent sends replies to the From address.
smtpobject- The account's own mail server an address sends through. Tito signs in to it before anything is saved, and keeps the password sealed where only its mail relay can read it: it is never answered and never written to the API's request log.
smtp.securitystring required- Signing in always needs TLS:
starttlsrequires the server to offer it,tlsconnects over it from the start. - one of
starttlstls smtp.passwordstring required- Required, every time: changing any of the server's details asks for the password again.
Response fields
idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
domainstringfrom_namestring- The display name. At most 100 characters, and never containing a control character.
statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive livebooleanstatus_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. records[].namestringrecords[].valuestringrecords[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. last_checkedstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladdressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. addresses[].smtp.portintegeraddresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused addresses[].created_atstring (RFC 3339) or null
Responses
200The sender, with the records to publish.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409This instance has no relay to reach the mail service through (unavailable), or the domain already sends from another Tito account, or is verified with the mail service by somebody else (domain_claimed), or the account already sends from this domain the other way (sender_mode). None changes on a retry; Tito support can move a sender between accounts.422A field is missing or unusable (invalid_body); every bad field is named at once, ansmtpone assmtp.host,smtp.port,smtp.security,smtp.usernameorsmtp.password. Or the mail server refused Tito's check (smtp_refused): the message names the field it is about, the reason (auth,tls,connect,timeout,blocked,refusedorbad_request) and the server's own reply.500Internal error.502The mail service could not be reached; nothing was changed (relay_unavailable).
curl -X POST https://<yourslug>.go.tito.io/api/v1/email-senders \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"domain":"tickets.example.com","from_local":"tickets","from_name":"The Summit"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/email-senders", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit"
}),
});
const data = await res.json();
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
Remove a sending address
DELETE /email-senders/{id}
Takes one sender down and answers the list as it now stands. Events that chose it follow the default again. Removing the default leaves the account with none, so its default mail goes out from default_from; no other sender is promoted without somebody choosing it.
The mail service is told first: a sender dropped before a failed call would leave the account unable to see or retry the one it still has.
Requires settings.manage
Path parameters
idinteger required- The sender's
id.
Response fields
availableboolean- Whether this instance can have senders at all. False means the feature is absent here.
default_fromstring- What mail goes out from when neither the event nor the account has a sender of its own.
senders[].idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. senders[].is_defaultboolean- True for the sender every event whose
email_sender_idis 0 sends from. senders[].configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
senders[].availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
senders[].domainstringsenders[].from_namestring- The display name. At most 100 characters, and never containing a control character.
senders[].statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive senders[].livebooleansenders[].status_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. senders[].not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
senders[].not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
senders[].modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp senders[].recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. senders[].records[].namestringsenders[].records[].valuestringsenders[].records[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. senders[].last_checkedstring (RFC 3339) or nullsenders[].created_atstring (RFC 3339) or nullsenders[].addressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. senders[].addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. senders[].addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. senders[].addresses[].smtp.portintegersenders[].addresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls senders[].addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. senders[].addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
senders[].addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
senders[].addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused senders[].addresses[].created_atstring (RFC 3339) or null
Responses
200Every sender that remains.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.502The mail service could not be reached; the sender is unchanged (relay_unavailable).
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/email-senders/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/email-senders/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"available": true,
"default_from": "tickets@ti.to",
"senders": [
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
]
}
Get one sending address
GET /email-senders/{id}
One sender, with its status and the DNS records it needs.
Requires settings.manage
Path parameters
idinteger required- The sender's
id.
Response fields
idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
domainstringfrom_namestring- The display name. At most 100 characters, and never containing a control character.
statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive livebooleanstatus_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. records[].namestringrecords[].valuestringrecords[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. last_checkedstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladdressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. addresses[].smtp.portintegeraddresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused addresses[].created_atstring (RFC 3339) or null
Responses
200The sender.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/email-senders/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/email-senders/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
Add a From address to a sender
POST /email-senders/{id}/addresses
Puts another From address on a sender's domain. The mail service verifies the DOMAIN, so the address sends as soon as its sender is live, with nothing to publish. The same local part again replaces that address's name and Reply-To and keeps its id, so every event that chose it keeps it.
On an smtp sender a new address needs smtp, the server it sends through, and Tito signs in to it before anything is saved; on any other sender smtp is refused (409 sender_mode).
Requires settings.manage
Path parameters
idinteger required- The sender's
id.
Request body
reply_tostring- Optional: one plain address replies go to. Empty or absent sends replies to the From address.
smtpobject- The account's own mail server an address sends through. Tito signs in to it before anything is saved, and keeps the password sealed where only its mail relay can read it: it is never answered and never written to the API's request log.
smtp.securitystring required- Signing in always needs TLS:
starttlsrequires the server to offer it,tlsconnects over it from the start. - one of
starttlstls smtp.passwordstring required- Required, every time: changing any of the server's details asks for the password again.
Response fields
idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
domainstringfrom_namestring- The display name. At most 100 characters, and never containing a control character.
statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive livebooleanstatus_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. records[].namestringrecords[].valuestringrecords[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. last_checkedstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladdressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. addresses[].smtp.portintegeraddresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused addresses[].created_atstring (RFC 3339) or null
Responses
200The sender, its addresses included.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The sender sends the other way (sender_mode):smtpon a sender that sends through Tito's mail service, or the reverse. Or this instance has no relay to reach the mail server through (unavailable).422A field is missing or unusable (invalid_body); every bad field is named at once, ansmtpone assmtp.hostand so on, and a new address on ansmtpsender withoutsmtpis refused. Or the mail server refused Tito's check (smtp_refused, as onPOST /email-senders).500Internal error.502The relay could not be reached; nothing was changed (relay_unavailable).
curl -X POST https://<yourslug>.go.tito.io/api/v1/email-senders/1/addresses \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"from_local":"forum","from_name":"The Summit Forum"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/email-senders/1/addresses", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"from_local": "forum",
"from_name": "The Summit Forum"
}),
});
const data = await res.json();
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
Remove a From address from a sender
DELETE /email-senders/{id}/addresses/{address_id}
Every event that chose the address follows the account's default again. When the address was its domain's primary (the one from_local names), the domain's oldest other address takes over, and becomes the account's default if the removed one was. The domain's only address can't be removed on its own: DELETE /email-senders/{id} removes the sender.
Requires settings.manage
Path parameters
idinteger required- The sender's
id. address_idinteger required- The address's
id, from the sender'saddresses.
Response fields
idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
domainstringfrom_namestring- The display name. At most 100 characters, and never containing a control character.
statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive livebooleanstatus_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. records[].namestringrecords[].valuestringrecords[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. last_checkedstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladdressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. addresses[].smtp.portintegeraddresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused addresses[].created_atstring (RFC 3339) or null
Responses
200The sender as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409This is the sender's only address (last_address).500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/email-senders/1/addresses/{address_id} \
-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/email-senders/1/addresses/{address_id}", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
Make a From address the default
POST /email-senders/{id}/addresses/{address_id}/default
Every event whose email_sender_address_id is 0 sends as the default. Its sender becomes the default sender, and the address its primary.
Requires settings.manage
Path parameters
idinteger required- The sender's
id. address_idinteger required- The address's
id, from the sender'saddresses.
Response fields
availableboolean- Whether this instance can have senders at all. False means the feature is absent here.
default_fromstring- What mail goes out from when neither the event nor the account has a sender of its own.
senders[].idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. senders[].is_defaultboolean- True for the sender every event whose
email_sender_idis 0 sends from. senders[].configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
senders[].availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
senders[].domainstringsenders[].from_namestring- The display name. At most 100 characters, and never containing a control character.
senders[].statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive senders[].livebooleansenders[].status_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. senders[].not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
senders[].not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
senders[].modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp senders[].recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. senders[].records[].namestringsenders[].records[].valuestringsenders[].records[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. senders[].last_checkedstring (RFC 3339) or nullsenders[].created_atstring (RFC 3339) or nullsenders[].addressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. senders[].addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. senders[].addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. senders[].addresses[].smtp.portintegersenders[].addresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls senders[].addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. senders[].addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
senders[].addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
senders[].addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused senders[].addresses[].created_atstring (RFC 3339) or null
Responses
200Every sender, the new default first.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/email-senders/1/addresses/{address_id}/default \
-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/email-senders/1/addresses/{address_id}/default", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"available": true,
"default_from": "tickets@ti.to",
"senders": [
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
]
}
Change the mail server an address sends through
POST /email-senders/{id}/addresses/{address_id}/smtp
Replaces the whole server an address on an smtp sender sends through, password included: the server and the password are sealed together, so changing either asks for both. Tito signs in to the new server first; if that fails, the address keeps the server it had.
Requires settings.manage
Path parameters
idinteger required- The sender's
id. address_idinteger required- The address's
id, from the sender'saddresses.
Request body
securitystring required- Signing in always needs TLS:
starttlsrequires the server to offer it,tlsconnects over it from the start. - one of
starttlstls passwordstring required- Required, every time: changing any of the server's details asks for the password again.
Response fields
idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
domainstringfrom_namestring- The display name. At most 100 characters, and never containing a control character.
statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive livebooleanstatus_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. records[].namestringrecords[].valuestringrecords[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. last_checkedstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladdressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. addresses[].smtp.portintegeraddresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused addresses[].created_atstring (RFC 3339) or null
Responses
200The sender, its addresses included.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The address's sender sends through Tito's mail service, not a mail server of its own (sender_mode), or this instance has no relay to reach one through (unavailable).422A field is missing or unusable (invalid_body, namingsmtp.hostand so on), or the mail server refused Tito's check (smtp_refused, as onPOST /email-senders).500Internal error.502The relay could not be reached; nothing was changed (relay_unavailable).
curl -X POST https://<yourslug>.go.tito.io/api/v1/email-senders/1/addresses/{address_id}/smtp \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"host":"smtp.example.com","port":587,"security":"starttls","password":"app-password"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/email-senders/1/addresses/{address_id}/smtp", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"password": "app-password"
}),
});
const data = await res.json();
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
Check an address's mail server
POST /email-senders/{id}/addresses/{address_id}/smtp/check
Signs in to the address's mail server and sends nothing. When it works, smtp.checked_at moves and smtp.not_sending clears. When the server refuses in a way the account can fix (the sign-in, TLS, a host Tito won't connect to), the address is marked not_sending, as a refused send would; a server that is down or slow is answered but not recorded. POST /email-senders/{id}/check checks every address on an smtp sender at once.
Requires settings.manage
Path parameters
idinteger required- The sender's
id. address_idinteger required- The address's
id, from the sender'saddresses.
Response fields
idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
domainstringfrom_namestring- The display name. At most 100 characters, and never containing a control character.
statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive livebooleanstatus_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. records[].namestringrecords[].valuestringrecords[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. last_checkedstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladdressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. addresses[].smtp.portintegeraddresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused addresses[].created_atstring (RFC 3339) or null
Responses
200The sender, its addresses included.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The address has no mail server of its own to check (no_smtp), or this instance has no relay (unavailable).422The mail server refused (smtp_refused): the message names the field it is about, the reason and the server's own reply.500Internal error.502The relay could not be reached (relay_unavailable).
curl -X POST https://<yourslug>.go.tito.io/api/v1/email-senders/1/addresses/{address_id}/smtp/check \
-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/email-senders/1/addresses/{address_id}/smtp/check", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
Ask where one sender's verification stands
POST /email-senders/{id}/check
The same check as POST /email-sender/check, for one sender. A caller cannot ask for live: only the all-green check writes it. On an smtp sender it signs in to each address's mail server instead, and what each found is on that address's smtp.
Requires settings.manage
Path parameters
idinteger required- The sender's
id.
Response fields
idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
domainstringfrom_namestring- The display name. At most 100 characters, and never containing a control character.
statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive livebooleanstatus_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. records[].namestringrecords[].valuestringrecords[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. last_checkedstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladdressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. addresses[].smtp.portintegeraddresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused addresses[].created_atstring (RFC 3339) or null
Responses
200The sender as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/email-senders/1/check \
-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/email-senders/1/check", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
Make a sending address the default
POST /email-senders/{id}/default
Every event whose email_sender_id is 0 sends from the default, as its primary address. The old default stays a sender. POST /email-senders/{id}/addresses/{address_id}/default picks the address too.
Requires settings.manage
Path parameters
idinteger required- The sender's
id.
Response fields
availableboolean- Whether this instance can have senders at all. False means the feature is absent here.
default_fromstring- What mail goes out from when neither the event nor the account has a sender of its own.
senders[].idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. senders[].is_defaultboolean- True for the sender every event whose
email_sender_idis 0 sends from. senders[].configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
senders[].availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
senders[].domainstringsenders[].from_namestring- The display name. At most 100 characters, and never containing a control character.
senders[].statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive senders[].livebooleansenders[].status_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. senders[].not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
senders[].not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
senders[].modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp senders[].recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. senders[].records[].namestringsenders[].records[].valuestringsenders[].records[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. senders[].last_checkedstring (RFC 3339) or nullsenders[].created_atstring (RFC 3339) or nullsenders[].addressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. senders[].addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. senders[].addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. senders[].addresses[].smtp.portintegersenders[].addresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls senders[].addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. senders[].addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
senders[].addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
senders[].addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused senders[].addresses[].created_atstring (RFC 3339) or null
Responses
200Every sender, the new default first.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/email-senders/1/default \
-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/email-senders/1/default", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"available": true,
"default_from": "tickets@ti.to",
"senders": [
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
]
}
Connections
What is connected
GET /connections
Every account-scoped provider the relay offers, connected or not. Use its credentials route for a pasted key or its authorize route for browser consent. Request and response bodies for connection routes are omitted from API logs.
Requires payment.manage
Response fields
connectionsarray of Connectionconnections[].categorystring- What kind of service it is, from the relay's manifest — "payments", "networking", "lead-retrieval" and so on.
connections[].kindstring- How this service is connected:
oauth,pat(a pasted key),both, orpartner_referral. Use the corresponding credential or authorization API route for this provider kind. connections[].connectedbooleanconnections[].connected_atstring (RFC 3339)connections[].stripe_accountsarray of StripeAccount- Stripe only: every Stripe account this account holds, the first connected first.
account_idis the default among them, the one new events pay into. Absent for every other service. connections[].stripe_accounts[].idstringconnections[].stripe_accounts[].namestring- What Stripe calls the account, empty when it has no name.
connections[].account_idstring- The provider-side identifier worth showing: a Stripe account or a PayPal merchant id. Not a credential — it is on the organizer's own screen and in the provider's dashboard — and empty for a service that has neither.
connections[].sheetobject- The spreadsheet a Google Sheets connection writes to, null until one has been made. The id is not a secret: it is in the address bar of anyone who opens the sheet.
connections[].sheet.idstringconnections[].sheet.urlstringconnections[].livemodeboolean- On a connected Stripe row: true for a live Stripe account, which takes real payments, and false for a Stripe sandbox, which takes test payments only. Null on every other row, and on a Stripe row that is not connected.
Responses
200The services.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.500Internal error.503The connect relay is unreachable, so what is connected cannot be known (relay_unavailable). Deliberately not an empty list: that would read as "this account has no integrations".
curl https://<yourslug>.go.tito.io/api/v1/connections \
-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/connections", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"connections": [
{
"id": "stripe",
"name": "Stripe",
"category": "payments",
"kind": "oauth",
"connected": true,
"connected_at": "2026-09-01T10:04:00Z",
"stripe_accounts": [
{
"id": "acct_1MEXAMPLE",
"name": "Example Events Ltd",
"livemode": true,
"default": true,
"events": 4
}
],
"account_id": "acct_1MEXAMPLE",
"sheet": {
"id": "1AbCdEf",
"url": "https://docs.google.com/spreadsheets/d/1AbCdEf/edit"
},
"livemode": true
}
]
}
Make the Google spreadsheet
POST /connections/google-sheets/sheet
Creates the spreadsheet a Google Sheets workflow writes to, on the connected account's Drive, and records which one it is — after this the connection's sheet names it.
This verb exists because Google grades its scopes: the narrow one we ask for reaches only files this app created, so an organizer cannot hand over a spreadsheet they already have. Making one is the only way to have one.
A connection that already names a spreadsheet is refused rather than given a second one.
Requires payment.manage
Response fields
Responses
201The spreadsheet.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404Google Sheets is not connected (not_connected).409This connection already has a spreadsheet (sheet_exists).500Internal error.502Google refused, or answered something unreadable (sheet_create_failed).
curl -X POST https://<yourslug>.go.tito.io/api/v1/connections/google-sheets/sheet \
-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/connections/google-sheets/sheet", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": "1AbCdEf",
"url": "https://docs.google.com/spreadsheets/d/1AbCdEf/edit"
}
Disconnect one Stripe account
DELETE /connections/stripe/accounts/{acct}
Disconnects one of the account's Stripe accounts. The events paying into it move to move_to, or to the default among the rest when it is absent. Orders paid into it keep naming it, and refunds for them are made in Stripe from then on. Disconnecting the last one disconnects Stripe.
Requires payment.manage
Path parameters
acctstring required- One of the account's Stripe accounts, as the Stripe connection's
stripe_accountslists it.
Query parameters
move_tostring- Another of the account's Stripe accounts, for the events paying into this one.
Response fields
Responses
200The account's Stripe accounts as they are now.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404Not one of this account's Stripe accounts (not_found).422move_tois not another of this account's Stripe accounts (invalid_body).500Internal error.502The provider refused to revoke the connection; it is still connected (revoke_refused).
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/connections/stripe/accounts/{acct} \
-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/connections/stripe/accounts/{acct}", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"stripe_accounts": [
{
"id": "acct_1MEXAMPLE",
"name": "Example Events Ltd",
"livemode": true,
"default": true,
"events": 4
}
]
}
Make a Stripe account the default
POST /connections/stripe/accounts/{acct}/default
New events pay into this Stripe account from now on. No event's card payments move: with more than one account connected, every event already names its own.
Requires payment.manage
Path parameters
acctstring required- One of the account's Stripe accounts, as the Stripe connection's
stripe_accountslists it.
Response fields
Responses
200The account's Stripe accounts as they are now.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404Not one of this account's Stripe accounts (not_found).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/connections/stripe/accounts/{acct}/default \
-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/connections/stripe/accounts/{acct}/default", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"stripe_accounts": [
{
"id": "acct_1MEXAMPLE",
"name": "Example Events Ltd",
"livemode": true,
"default": true,
"events": 4
}
]
}
Disconnect a service
DELETE /connections/{service}
Revokes the connection at the provider FIRST, then removes it here. A revoke the provider or the relay refuses leaves everything exactly as it was — the connection stays, and this answers 502 — because deleting first would lose the account's only handle on a connection the provider still honours.
Every row the external-sync ledger holds for this service is then marked failed, keeping its remote id so a reconnect adopts it, and the payment rail is re-resolved on the spot: no restart, and Stripe drops back to a direct key if the instance has one.
The organizer's screen warns about something a caller is not told: disconnecting the account's last payment provider stops paid ticket sales dead. A key with payment.manage is trusted with that, the way the screen is.
Requires payment.manage
Path parameters
servicestring required- The service's id, as
GET /connectionsgives it.
Response fields
disconnectedboolean
Responses
200It is disconnected.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such service on this account, or it is not connected (not_found,not_connected).409The account has several Stripe accounts; disconnect one withDELETE /connections/stripe/accounts/{acct}(several_accounts).500Internal error.502The provider refused to revoke the connection; it is still connected (revoke_refused).503The connect relay is unreachable, so what is connected cannot be known (relay_unavailable). Deliberately not an empty list: that would read as "this account has no integrations".
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/connections/{service} \
-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/connections/{service}", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"disconnected": true
}
One service
GET /connections/{service}
The same row GET /connections lists — worth polling after somebody has been sent to the Integrations screen to connect.
An event-scoped service answers 404 here, the same as an unknown one: this is the account's door, and it names the account's own rows only.
Requires payment.manage
Path parameters
servicestring required- The service's id, as
GET /connectionsgives it.
Response fields
categorystring- What kind of service it is, from the relay's manifest — "payments", "networking", "lead-retrieval" and so on.
kindstring- How this service is connected:
oauth,pat(a pasted key),both, orpartner_referral. Use the corresponding credential or authorization API route for this provider kind. connectedbooleanconnected_atstring (RFC 3339)stripe_accountsarray of StripeAccount- Stripe only: every Stripe account this account holds, the first connected first.
account_idis the default among them, the one new events pay into. Absent for every other service. stripe_accounts[].idstringaccount_idstring- The provider-side identifier worth showing: a Stripe account or a PayPal merchant id. Not a credential — it is on the organizer's own screen and in the provider's dashboard — and empty for a service that has neither.
sheetobject- The spreadsheet a Google Sheets connection writes to, null until one has been made. The id is not a secret: it is in the address bar of anyone who opens the sheet.
sheet.idstringsheet.urlstringlivemodeboolean- On a connected Stripe row: true for a live Stripe account, which takes real payments, and false for a Stripe sandbox, which takes test payments only. Null on every other row, and on a Stripe row that is not connected.
Responses
200The service.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.503The connect relay is unreachable, so what is connected cannot be known (relay_unavailable). Deliberately not an empty list: that would read as "this account has no integrations".
curl https://<yourslug>.go.tito.io/api/v1/connections/{service} \
-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/connections/{service}", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": "stripe",
"name": "Stripe",
"category": "payments",
"kind": "oauth",
"connected": true,
"connected_at": "2026-09-01T10:04:00Z",
"stripe_accounts": [
{
"id": "acct_1MEXAMPLE",
"name": "Example Events Ltd",
"livemode": true,
"default": true,
"events": 4
}
],
"account_id": "acct_1MEXAMPLE",
"sheet": {
"id": "1AbCdEf",
"url": "https://docs.google.com/spreadsheets/d/1AbCdEf/edit"
},
"livemode": true
}
Start provider browser authorization
POST /connections/{service}/authorize
Supports OAuth, combined OAuth and key providers, and partner referrals. Returns an HTTPS URL with Cache-Control: no-store. The person must finish consent in the browser. Provider failures are logged without credential bodies. Connection request and response bodies are omitted from API logs. Credentials are never returned. The URL is a short-lived bearer secret. Keep it out of chat and logs.
Requires payment.manage
Path parameters
servicestring required
Response fields
Responses
200Connection result401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid input, provider kind or provider unavailable in this environment500Internal error.502The relay could not complete the request503The relay provider catalog is unavailable
curl -X POST https://<yourslug>.go.tito.io/api/v1/connections/{service}/authorize \
-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/connections/{service}/authorize", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"service": "example",
"authorize_url": "https://example.com/hooks/tito"
}
Store a provider credential
POST /connections/{service}/credentials
Stores a trimmed key of 1 to 4096 bytes through the relay. The provider must accept pasted keys in this scope. Stored does not mean the provider has verified the key. Successful responses have Cache-Control: no-store. Provider failures are logged without credential bodies. Connection request and response bodies are omitted from API logs. Credentials are never returned.
Requires payment.manage
Path parameters
servicestring required
Request body
keystring required
Response fields
servicestring
Responses
200Connection result401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid input, provider kind or provider unavailable in this environment500Internal error.502The relay could not complete the request503The relay provider catalog is unavailable
curl -X POST https://<yourslug>.go.tito.io/api/v1/connections/{service}/credentials \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"key":"example"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/connections/{service}/credentials", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"key": "example"
}),
});
const data = await res.json();
{
"service": "example",
"status": "stored"
}
Webhooks
Add a webhook endpoint
POST /webhooks
A new endpoint, enabled and ready to deliver. url must be an https address on port 443 that resolves to a public host — the same SSRF rail the deliveries themselves keep. event_types needs at least one of the events this account sends; an empty list is REFUSED rather than read as "all of them", because an endpoint subscribed to nothing is a URL that never fires.
The signing secret is never served — on creation or after. It is stored in plaintext because it has to be recoverable to sign outgoing requests, so unlike an API key it could be handed back at any time; that is exactly why the refusal is deliberate. And this API's request log stores every response body verbatim for 90 days, so a secret in an answer would be written into the account's own database and sit there.
The consequence, stated rather than hidden: an endpoint created here is real and will deliver, but the string that verifies its signatures is read off the organizer's own Webhooks screen. A script can set the integration up; a person collects the one secret.
An endpoint's event types can be changed later with PATCH /webhooks/{id}. Its URL cannot be changed on any surface: a different address is a new endpoint.
Requires settings.manage
Request body
event_typesarray of string required- At least one of the events this account sends. An empty list is refused.
Response fields
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.
Responses
201The endpoint. No secret.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422urlis not an https address on port 443,event_typesis empty or names an unknown event, or the body carried an explicit null or unknown field (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/webhooks \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/tito-hook","event_types":["ticket"]}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/webhooks", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://example.com/tito-hook",
"event_types": [
"ticket"
]
}),
});
const data = await res.json();
{
"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
}
Delete an endpoint
DELETE /webhooks/{id}
Removes the endpoint and stops its deliveries. The organizer's own screen puts a confirmation in front of this; a caller gets none.
Requires settings.manage
Path parameters
idinteger required- The endpoint's id, as
GET /webhookslists it.
Response fields
deletedboolean
Responses
200It is gone.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/webhooks/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/webhooks/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"deleted": true
}
Change an endpoint's events, or pause or resume it
PATCH /webhooks/{id}
Both fields are optional, and one left out is left as it is. There is no url: an endpoint's address is not editable on any surface.
event_types REPLACES the endpoint's list, under the same rule as creating one: at least one, and only events this account sends. The list is written before enabled, so a refused list changes nothing. Deliveries already queued go out as they were queued.
Resuming sends the backlog. Every delivery that piled up while the endpoint was off goes back on the clock, so a caller switching one back on should expect the outage's events to arrive. That holds whether it was paused by hand or disabled automatically after repeated failures — a disable holds the queue rather than discarding it.
Requires settings.manage
Path parameters
idinteger required- The endpoint's id, as
GET /webhookslists it.
Request body
event_typesarray of string- The events this endpoint should get from now on, replacing what it had. At least one. An empty list is refused.
enabledboolean
Response fields
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.
Responses
200The endpoint as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422event_typesis empty or names an unknown event, or the body carried an explicit null or a field that is notevent_typesorenabled(invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/webhooks/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"event_types":["ticket"]}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/webhooks/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"event_types": [
"ticket"
]
}),
});
const data = await res.json();
{
"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
}
Replay a delivery
POST /webhooks/{id}/redeliver
Queues a recorded delivery to go out again, exactly as it was first built. The payload is copied verbatim: this replays the event as it WAS, not as the account looks now.
It is a NEW delivery, never a revived one — what happened the first time is evidence, and a replay that also fails must not overwrite it.
Refused while the endpoint is off. A queued row against a disabled endpoint cannot be claimed by design, so accepting one would answer "sent" to something that will not move until an unrelated switch is flipped.
Requires settings.manage
Path parameters
idinteger required- The endpoint's id, as
GET /webhookslists it.
Request body
delivery_idinteger required- A delivery of THIS endpoint, as
GET /webhooks/{id}/deliverieslists it.
Response fields
queuedboolean
Responses
200It is queued.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The endpoint is off, so a queued delivery could not be sent (endpoint_off).422delivery_idwas missing (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/webhooks/1/redeliver \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"delivery_id":41}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/webhooks/1/redeliver", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"delivery_id": 41
}),
});
const data = await res.json();
{
"queued": true
}
Send a test delivery
POST /webhooks/{id}/test
Posts a test delivery to the endpoint NOW and reports what came back. Synchronous on purpose: everything else here queues, and this is the one verb whose whole question is "did my server accept it?".
The delivery is recorded either way, so a failed test sits in the history beside the real ones and can be inspected through GET /webhooks/{id}/deliveries.
Requires settings.manage
Path parameters
idinteger required- The endpoint's id, as
GET /webhookslists it.
Response fields
Responses
200What the endpoint answered.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/webhooks/1/test \
-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/test", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"delivery_id": 88,
"status": "succeeded",
"status_code": 200,
"error": ""
}
Workflows
Make a workflow
POST /workflows
A new workflow is disabled, always — whichever door made it. A template can carry a placeholder URL, and a definition that started calling out before anybody read it is the failure that rule exists for. Switching it on is a separate, refusable act.
What a workflow DOES is not authored here, and that is a recorded gap rather than an oversight. Its trigger, its condition, its actions and the nodes between them are a GRAPH, built one clause at a time through the builder's own saves, each validating against a vocabulary of nodes and mappings. Exposing that as a JSON body means designing a public schema for the graph — a bigger question than a field list. What a caller can do today is stamp a TEMPLATE: a whole working definition, chosen by name.
Say which events it covers, with events or all_events. A workflow fires only for the events it covers, so one made with neither is switched on to no effect.
The answer carries the definition, so a caller can read what a template gave it before deciding to switch it on.
Requires settings.manage
Request body
templatestring- Create only: stamps a whole working definition by name. The copy is the account's own from that moment — no link back, so a later change to the template never reaches a live config.
eventsarray of string- The slugs of the events this workflow runs for. Replaces the ones it covers, the same as ticking them in the builder, and an empty list covers none. A workflow fires only for the events it covers, so a template stamped without this or
all_eventsis switched on to no effect. A slug this account does not have refuses the whole request with422 invalid_body. all_eventsbooleantruecovers every event, including ones made later. Sent together witheventsit is refused.falseon its own covers none.enabledboolean- Patch only. Turning it on is refused unless the workflow could actually run; turning it off also stops the queued work.
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
201The workflow, switched off.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422templatenames one this build does not have,eventsnames a slug this account does not have,all_events: truewas sent together withevents, or the body carried an explicit null or unknown field (invalid_body). Nothing is made.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/workflows \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Slack when a ticket sells"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/workflows", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Slack when a ticket sells"
}),
});
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"
}
Delete a workflow
DELETE /workflows/{id}
Takes the workflow's batches, runs and subscriptions with it, in one transaction — a workflow can never be half-gone with runs pointing at nothing. The organizer's own screen puts a confirmation in front of this; a caller gets none.
Requires settings.manage
Response fields
deletedboolean
Responses
200It is gone.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/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: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"deleted": true
}
Rename a workflow, change its events, or switch it on or off
PATCH /workflows/{id}
name, the events it covers (events or all_events) and enabled are the fields — the definition is the builder's, per the note on creating. The events are written before the switch, so one PATCH can point a workflow at its events and turn it on.
Switching ON is refusable. The definition has to parse, validate against the actions this build knows, and read as finished, so a caller cannot arm a workflow whose first act would be to fail. 409 not_ready is that refusal.
Switching OFF stops the queued work, not just the column. Runs already mid-request finish; everything merely queued stops, and any running batch is cancelled. A caller turning a misbehaving workflow off should expect it to actually stop — writing the column alone once left an account posting to somebody else's system for hours.
Requires settings.manage
Request body
templatestring- Create only: stamps a whole working definition by name. The copy is the account's own from that moment — no link back, so a later change to the template never reaches a live config.
eventsarray of string- The slugs of the events this workflow runs for. Replaces the ones it covers, the same as ticking them in the builder, and an empty list covers none. A workflow fires only for the events it covers, so a template stamped without this or
all_eventsis switched on to no effect. A slug this account does not have refuses the whole request with422 invalid_body. all_eventsbooleantruecovers every event, including ones made later. Sent together witheventsit is refused.falseon its own covers none.enabledboolean- Patch only. Turning it on is refused unless the workflow could actually run; turning it off also stops the queued work.
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 as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409It is not finished, so it cannot be switched on (not_ready).422The body named something that is notname,events,all_eventsorenabled,eventsnames a slug this account does not have, orall_events: truewas sent together withevents(invalid_body). Nothing is changed.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/workflows/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Slack when a ticket sells"}'
// 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: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Slack when a ticket sells"
}),
});
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"
}
Widget websites
Add a website
POST /widget-domains
Approves a website before the widget has loaded there, so it renders on the first visit. domain takes anything that names a website (example.com, https://www.example.com/tickets, EXAMPLE.COM.) and is reduced to the same stored form a sighting of that site records: lowercase host, no scheme, no path, no trailing dot, no leading www., and a port only when it isn't the default. One row covers both the bare and the www. form.
What happens depends on whether the website is already on the list:
- Not on it: 201 and the new row, approved, with sightings 0 and first_seen_at and last_seen_at null. - Waiting for an answer (pending): it is approved. 200 and the row. - Already approved: nothing changes. 200 and the row, so repeating the call is safe. - blocked: nothing changes. 409 blocked. To unblock it, PATCH /widget-domains/{id} with {"status":"approved"}.
The decision is ACCOUNT-WIDE, like the PATCH: it covers every widget on every event. Refused in an account sandbox, where approving a website is set-up and belongs to the live account.
Requires events.manage
Request body
domainstring required- Required. A host or a URL; it is stored as the bare host, and covers the
www.form too.
Response fields
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 sightingsinteger- How many times the widget has loaded there. 0 on a website added with
POST /widget-domainsuntil it first does. first_seen_atstring (RFC 3339) or null- When the widget first loaded there; null on a website added by hand that it hasn't loaded on yet.
last_seen_atstring (RFC 3339) or null- When the widget last loaded there; null on a website added by hand that it hasn't loaded on yet.
decided_atstring (RFC 3339) or null- When somebody answered about this website; null while nobody has.
Response fields
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 sightingsinteger- How many times the widget has loaded there. 0 on a website added with
POST /widget-domainsuntil it first does. first_seen_atstring (RFC 3339) or null- When the widget first loaded there; null on a website added by hand that it hasn't loaded on yet.
last_seen_atstring (RFC 3339) or null- When the widget last loaded there; null on a website added by hand that it hasn't loaded on yet.
decided_atstring (RFC 3339) or null- When somebody answered about this website; null while nobody has.
Responses
201The website was added, approved.200The website was already on the list: approved now if it was waiting, unchanged if it was already approved.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.409The website is blocked (blocked). Nothing changed.422domainwas missing or null, or the body carried an unknown field (invalid_body);domaindoesn't name a website: blank, a wildcard,null, or a path with no host (invalid_domain).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/widget-domains \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"domain":"https://www.techarena.se/tickets"}'
// 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: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"domain": "https://www.techarena.se/tickets"
}),
});
const data = await res.json();
The website was added, approved.
{
"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",
"decided_at": "2026-08-27T18:20:00Z"
}
The website was already on the list: approved now if it was waiting, unchanged if it was already approved.
{
"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",
"decided_at": "2026-08-27T18:20:00Z"
}
Approve or block a website
PATCH /widget-domains/{id}
Answers "someone used your widget on X — is that you?". The decision is ACCOUNT-WIDE: it covers every widget on every event, which is why this route is not event-scoped. status is approved, blocked, or pending — pending is not only the starting state, it is how a decision is TAKEN BACK, and the only way back for a website blocked by mistake.
A website reaches the list by being SEEN (it starts pending) or by POST /widget-domains (it starts approved). There is no delete: blocking is the answer, and the row stays.
Blocking the last approved website takes every widget on the account off every site at once, because the browser rule falls back to frame-ancestors 'none'. The organizer's own screen puts a confirmation in front of that; this does not, because a confirmation is a person being asked and a caller has already decided.
Requires events.manage
Path parameters
idinteger required- The website's id, as
GET /widget-domainslists it.
Request body
Response fields
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 sightingsinteger- How many times the widget has loaded there. 0 on a website added with
POST /widget-domainsuntil it first does. first_seen_atstring (RFC 3339) or null- When the widget first loaded there; null on a website added by hand that it hasn't loaded on yet.
last_seen_atstring (RFC 3339) or null- When the widget last loaded there; null on a website added by hand that it hasn't loaded on yet.
decided_atstring (RFC 3339) or null- When somebody answered about this website; null while nobody has.
Responses
200The website and its standing.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422statuswas missing, null, or not one of the three (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/widget-domains/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"status":"approved"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/widget-domains/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"status": "approved"
}),
});
const data = await res.json();
{
"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",
"decided_at": "2026-08-27T18:20:00Z"
}
API keys
List the account's API keys
GET /api-keys
Newest first, revoked keys included. No list response carries a secret or its hash: display names a key without revealing it. Delegated keys are included, but agent grants are listed separately.
Requires settings.manage
Response fields
api_keysarray of APIKeyapi_keys[].idintegerapi_keys[].displaystring- The minting prefix plus the stored 8-character fragment — really the secret's leading substring, which is why it ends in an ellipsis. Enough to tell two keys apart, not enough to use.
api_keys[].capabilitiesarray of string- The scope strings this key holds — the same names a developer pastes into an integration. A key is capped by whoever minted it.
api_keys[].revoked_atstring (RFC 3339) or nullapi_keys[].created_atstring (RFC 3339) or nullapi_keys[].parent_key_idinteger or null- The key that delegated this key. Null for keys created in the organizer UI.
Responses
200The keys.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/api-keys \
-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/api-keys", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"api_keys": [
{
"id": 2,
"name": "Zapier",
"display": "titogo_sk_A3F9KM2P…",
"capabilities": [
"events.view",
"orders.view"
],
"revoked": false,
"revoked_at": null,
"expired": false,
"expires_at": null,
"last_used_at": "2026-09-08T14:02:00Z",
"created_at": "2026-08-01T09:00:00Z",
"parent_key_id": 1
}
]
}
Create a short-lived API key
POST /api-keys
The caller must be a human-created API key with settings.manage. Requested capabilities must be a subset of the caller’s and the requested 1–30 day lifetime must not outlast it. Delegated keys cannot delegate again; connected AI agents cannot mint keys. Revoking the parent closes its children. The secret appears only in this 201 response, which is no-store and omitted from API request logs. The response includes parent_key_id in api_key.
Requires settings.manage
Request body
Response fields
api_key.idintegerapi_key.displaystring- The minting prefix plus the stored 8-character fragment — really the secret's leading substring, which is why it ends in an ellipsis. Enough to tell two keys apart, not enough to use.
api_key.capabilitiesarray of string- The scope strings this key holds — the same names a developer pastes into an integration. A key is capped by whoever minted it.
api_key.revoked_atstring (RFC 3339) or nullapi_key.created_atstring (RFC 3339) or nullapi_key.parent_key_idinteger or null- The key that delegated this key. Null for keys created in the organizer UI.
secretstring
Responses
201New key and once-only secret.401No key, an unknown key, or a revoked key.403Missing settings.manage or delegation_forbidden for an agent grant, a delegated caller, or capabilities beyond the caller.422Invalid name, lifetime, or request body; a child cannot outlive its parent.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/api-keys \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Ada Lovelace","capabilities":["example"],"expires_in_days":1}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/api-keys", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Ada Lovelace",
"capabilities": [
"example"
],
"expires_in_days": 1
}),
});
const data = await res.json();
{
"api_key": {
"id": 2,
"name": "Zapier",
"display": "titogo_sk_A3F9KM2P…",
"capabilities": [
"events.view",
"orders.view"
],
"revoked": false,
"revoked_at": null,
"expired": false,
"expires_at": null,
"last_used_at": "2026-09-08T14:02:00Z",
"created_at": "2026-08-01T09:00:00Z",
"parent_key_id": 1
},
"secret": "example"
}
Get an API key
GET /api-keys/{id}
One key, in the list's shape. Never its secret.
Requires settings.manage
Path parameters
idinteger required- The key's id, as
GET /api-keyslists it.
Response fields
idintegerdisplaystring- The minting prefix plus the stored 8-character fragment — really the secret's leading substring, which is why it ends in an ellipsis. Enough to tell two keys apart, not enough to use.
capabilitiesarray of string- The scope strings this key holds — the same names a developer pastes into an integration. A key is capped by whoever minted it.
revoked_atstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nullparent_key_idinteger or null- The key that delegated this key. Null for keys created in the organizer UI.
Responses
200The key.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/api-keys/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/api-keys/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 2,
"name": "Zapier",
"display": "titogo_sk_A3F9KM2P…",
"capabilities": [
"events.view",
"orders.view"
],
"revoked": false,
"revoked_at": null,
"expired": false,
"expires_at": null,
"last_used_at": "2026-09-08T14:02:00Z",
"created_at": "2026-08-01T09:00:00Z",
"parent_key_id": 1
}
Revoke an API key
POST /api-keys/{id}/revoke
Shuts the key's door. A POST rather than a DELETE because a key is never REMOVED: the request log names it, and a log row pointing at nothing is worse than a revoked key in a list. The organizer's own screen puts a confirmation in front of this; a caller gets none.
A key may revoke itself, on purpose — a key somebody thinks has leaked should be able to shut its own door without waiting for a human to sign in. The answer still comes back, because the credential was checked before the handler ran; every later call with it is refused.
Requires settings.manage
Path parameters
idinteger required- The key's id, as
GET /api-keyslists it.
Response fields
idintegerdisplaystring- The minting prefix plus the stored 8-character fragment — really the secret's leading substring, which is why it ends in an ellipsis. Enough to tell two keys apart, not enough to use.
capabilitiesarray of string- The scope strings this key holds — the same names a developer pastes into an integration. A key is capped by whoever minted it.
revoked_atstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nullparent_key_idinteger or null- The key that delegated this key. Null for keys created in the organizer UI.
Responses
200The key, now revoked.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409It was revoked already (revoked).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/api-keys/1/revoke \
-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/api-keys/1/revoke", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 2,
"name": "Zapier",
"display": "titogo_sk_A3F9KM2P…",
"capabilities": [
"events.view",
"orders.view"
],
"revoked": false,
"revoked_at": null,
"expired": false,
"expires_at": null,
"last_used_at": "2026-09-08T14:02:00Z",
"created_at": "2026-08-01T09:00:00Z",
"parent_key_id": 1
}
Read the API request log
GET /api-requests
The newest calls this account's keys have made. The log is pruned to 90 days.
The stored request and response bodies are not served. Those bodies are whatever this API answered — attendee names, addresses, field answers — so returning them would let a key holding only settings.manage read data that orders.view gates, and the log would become a way around the capability system rather than a record of it. The organizer's own screen shows them to a person whose ROLE already spans everything; a key is deliberately narrower than a role. What this serves is the log's shape: who called what, when, and with what answer.
Requires settings.manage
Query parameters
limitinteger- How many rows, 1 to 200. Default 50. Anything else is refused rather than clamped.
afterinteger- Cursor: the
idof the last request on the previous page. The page holds older calls than that one. Omit for the newest. qstring- Only calls whose method and path, written as
METHOD /path, contain this text. Case-insensitive. statusstring- Only calls answered with this status:
succeeded(any 2xx),failed(any 4xx or 5xx), or one three-digit code.
Response fields
requestsarray of APIRequestrequests[].idintegerrequests[].methodstringrequests[].pathstringrequests[].statusintegerrequests[].duration_msintegerrequests[].created_atstring (RFC 3339) or null
Responses
200The log.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422limitwas not an integer from 1 to 200, orstatuswas notsucceeded,failedor a three-digit code (invalid_query), orafterwas not a positive integer (invalid_after).500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/api-requests \
-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/api-requests", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"requests": [
{
"id": 8431,
"key_name": "Zapier",
"method": "GET",
"path": "/api/v1/events/summit-2027/tickets",
"status": 200,
"duration_ms": 14,
"created_at": "2026-09-08T14:02:00Z"
}
]
}
Events
Events are the things you sell tickets to, and series group them. Every other resource hangs off an event, addressed by its slug.
Events
List events
GET /events
Every event on the account, most recently created first, archived ones included unless archived says otherwise. An event's slug is what every /events/{slug} operation takes.
Requires events.view
Query parameters
archivedstringtruelists only archived events, the admin events list's Archived view.falselists only events that are not archived. Omit it for every event.1and0are read astrueandfalse; anything else is refused withinvalid_query.
Response fields
eventsarray of Eventevents[].admin_urlstring- Links to the event admin page, as an absolute URL: on the root domain, or on the instance's own address on a standalone account.
events[].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 map the event page shows first; people can switch it themselves, and their browser remembers what they picked.
autois Apple Maps on Apple devices, OpenStreetMap on Linux and Google Maps elsewhere. A stored preference, not a promise about what people see: a service the page cannot draw ("apple" without Apple credentials, "osm" without a map pin) falls back to one it can, and an event with no map at all shows the address and its open-in links. - one of
autoapplegoogleosm events[].starts_atstring (RFC 3339) or null- Null when the event has no start time set (stored as Unix 0).
events[].ends_atstring (RFC 3339) or null- When the event ends, in the same form as
starts_at. Null when the event has no end time set. events[].all_dayboolean- Whether the event's dates are shown without times. A date-only
starts_atsent to a create or a PATCH sets it. events[].date_textstring- Free text the event page shows instead of the dates ("June 2027", "TBC"). Empty when the dates are shown.
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[].unlistedboolean- True when the event is unlisted: anyone with the link. The event is absent from the account's public event list and from its series page, and nothing else changes — its main page answers everybody, it sells, and it is indexable unless search_indexing says otherwise. A listing leaves it out; a link to it keeps working. Independent of secret; where both are true, secret is the stronger answer.
events[].archivedboolean- True when the event has been archived (
POST /events/{slug}/archive). It is off the admin's everyday lists and the account's public event list and series pages, and its main page answers 404 to anyone not signed in, as a secret event's does. Orders, tickets and every link already sent keep working. A listing leaves it out. events[].archived_atstring (RFC 3339) or null- When the event was archived. Null while it is not archived.
events[].delete_onstring (RFC 3339) or null- When the event will be deleted, after the account's owner asked for it to be deleted in the admin. Null while it is not waiting to be deleted. An event with orders, tickets, invitations or ticket requests waits 30 days, and until then registration is closed: it is off the public lists, its main page answers 404 to anyone not signed in, and nothing can be bought or claimed.
POST /events/{slug}/keeptakes the request back and reopens registration. events[].anonymize_onstring (RFC 3339) or null- When the attendee data on the event will be anonymized, after the account's owner asked for it in the admin. Null while it is not waiting to be. It waits 30 days and registration stays open meanwhile, so anyone who registers before then is anonymized too. On the day every buyer's, attendee's and invitee's details are removed, and the orders, what they cost and when they were paid stay.
POST /events/{slug}/keep-datatakes the request back until then. events[].search_indexingstring- This event's OWN answer to whether search engines may list it. "inherit" means the account's search_indexing switch decides, which is where every event starts and most stay. "on" and "off" override it for this event only. Read no_index for the resolved answer.
- one of
inheritonoff events[].no_indexboolean- The RESOLVED answer, and the one to read: true when this event is asking search engines not to list it, whether that came from its own switch or from the account's. It closes nothing and gates nothing — the page answers exactly as it did, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells — so do not read it as a visibility gate the way you read draft. It is independent of secret in both directions. Resolved rather than raw because the raw field alone gets the common case backwards: the account's search_indexing is off until somebody turns it on, so an event that has never touched its own switch is out of search while its own setting still says "inherit".
events[].passed_params_onboolean- Whether the query params on a buyer's link are kept on the order they place. On by default. This is the switch and it is the only way off: writing an empty
passed_params_patternresets to the default rather than stopping capture. events[].passed_params_patternstring- Which param names are kept, as comma-separated globs anchored at both ends.
utm_*keepsutm_source,utm_mediumand the rest. Click ids such asgclidandfbclidare not in the default and are added by name, because a click id identifies a person where a campaign name identifies a campaign. events[].calendar_on_event_pageboolean- Whether the event page offers Add to calendar beside its date, before anybody books: Google Calendar, Outlook and a calendar file of the event's own date, carrying only what the page shows. Off until the organizer turns it on. Separate from
calendar_links_enabledonGET /events/{slug}/settings/after-registering, which is the offer after booking, on the order page, the ticket and the emails. events[].secret_redirect_urlstring- Where the event's page sends visitors while the event is
secret, as a 302 on the account's own addresses. Empty when there is none, and then a secret event's page is not found. Kept while the event is public, and only used while it is secret. Calendar entries link it in place of the event page while it is used. 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.422archivedwas nottrue,false,1or0(invalid_query).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": [
{
"admin_url": "https://go.tito.io/youraccount/annual-conf/admin",
"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",
"ends_at": "2026-10-15T17:00:00Z",
"all_day": false,
"date_text": "",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"updated_at": "2026-03-09T16:04:12Z",
"draft": false,
"secret": false,
"unlisted": false,
"archived": false,
"archived_at": "2026-03-09T16:04:12Z",
"delete_on": "2026-11-10T14:30:00Z",
"anonymize_on": "2026-11-10T14:30:00Z",
"search_indexing": "inherit",
"no_index": false,
"share_title": "",
"share_text": "",
"share_image_url": "/a/8f2c1d9e4b7a6350.png",
"passed_params_on": true,
"passed_params_pattern": "utm_*",
"calendar_on_event_page": false,
"secret_redirect_url": "https://example.com/tickets",
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
]
}
Create an event
POST /events
The machine door onto "New event". The event is born exactly as the form's is: one free, uncapped "General admission" ticket type, the default page sections, published (not a draft) and not secret. A blank slug derives from the name; a taken one is refused with slug_taken rather than silently suffixed, because a caller that named an address meant that address. An unknown currency is refused (invalid_currency) rather than falling back — a form has a picker, a caller has a typo. series names an existing series by slug and nests the event's address under it. starts_at and ends_at are 2006-01-02 (a date, which reads as all-day) or 2006-01-02T15:04 in the event's timezone; "" clears one. date_text is a free-text display override ("June 2027", "TBC") shown instead of the dates. Answers the event in the same shape GET /events/{slug} does.
Requires events.manage
Request body
slugstring- The address the event answers on. Derived from the name when blank; lowercase letters, digits and hyphens.
timezonestring- IANA zone the event's dates are read in and shown in. Default
UTC.Localis refused, because it names the server's clock rather than a place. currencystring- Lowercase ISO code; must be one an event can be created in. Default
eur. Fixed after creation. venue_latstring- Latitude as a decimal string from -90 to 90. Latitude and longitude must form a valid pair; empty strings clear the pin.
venue_lngstring- Longitude as a decimal string from -180 to 180. Latitude and longitude must form a valid pair; empty strings clear the pin.
Response fields
admin_urlstring- Links to the event admin page, as an absolute URL: on the root domain, or on the instance's own address on a standalone account.
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 map the event page shows first; people can switch it themselves, and their browser remembers what they picked.
autois Apple Maps on Apple devices, OpenStreetMap on Linux and Google Maps elsewhere. A stored preference, not a promise about what people see: a service the page cannot draw ("apple" without Apple credentials, "osm" without a map pin) falls back to one it can, and an event with no map at all shows the address and its open-in links. - one of
autoapplegoogleosm ends_atstring (RFC 3339) or null- When the event ends, in the same form as
starts_at. Null when the event has no end time set. all_dayboolean- Whether the event's dates are shown without times. A date-only
starts_atsent to a create or a PATCH sets it. date_textstring- Free text the event page shows instead of the dates ("June 2027", "TBC"). Empty when the dates are shown.
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".
unlistedboolean- True when the event is unlisted: anyone with the link. The event is absent from the account's public event list and from its series page, and nothing else changes — its main page answers everybody, it sells, and it is indexable unless search_indexing says otherwise. A listing leaves it out; a link to it keeps working. Independent of secret; where both are true, secret is the stronger answer.
archivedboolean- True when the event has been archived (
POST /events/{slug}/archive). It is off the admin's everyday lists and the account's public event list and series pages, and its main page answers 404 to anyone not signed in, as a secret event's does. Orders, tickets and every link already sent keep working. A listing leaves it out. delete_onstring (RFC 3339) or null- When the event will be deleted, after the account's owner asked for it to be deleted in the admin. Null while it is not waiting to be deleted. An event with orders, tickets, invitations or ticket requests waits 30 days, and until then registration is closed: it is off the public lists, its main page answers 404 to anyone not signed in, and nothing can be bought or claimed.
POST /events/{slug}/keeptakes the request back and reopens registration. anonymize_onstring (RFC 3339) or null- When the attendee data on the event will be anonymized, after the account's owner asked for it in the admin. Null while it is not waiting to be. It waits 30 days and registration stays open meanwhile, so anyone who registers before then is anonymized too. On the day every buyer's, attendee's and invitee's details are removed, and the orders, what they cost and when they were paid stay.
POST /events/{slug}/keep-datatakes the request back until then. search_indexingstring- This event's OWN answer to whether search engines may list it. "inherit" means the account's search_indexing switch decides, which is where every event starts and most stay. "on" and "off" override it for this event only. Read no_index for the resolved answer.
- one of
inheritonoff no_indexboolean- The RESOLVED answer, and the one to read: true when this event is asking search engines not to list it, whether that came from its own switch or from the account's. It closes nothing and gates nothing — the page answers exactly as it did, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells — so do not read it as a visibility gate the way you read draft. It is independent of secret in both directions. Resolved rather than raw because the raw field alone gets the common case backwards: the account's search_indexing is off until somebody turns it on, so an event that has never touched its own switch is out of search while its own setting still says "inherit".
passed_params_onboolean- Whether the query params on a buyer's link are kept on the order they place. On by default. This is the switch and it is the only way off: writing an empty
passed_params_patternresets to the default rather than stopping capture. passed_params_patternstring- Which param names are kept, as comma-separated globs anchored at both ends.
utm_*keepsutm_source,utm_mediumand the rest. Click ids such asgclidandfbclidare not in the default and are added by name, because a click id identifies a person where a campaign name identifies a campaign. calendar_on_event_pageboolean- Whether the event page offers Add to calendar beside its date, before anybody books: Google Calendar, Outlook and a calendar file of the event's own date, carrying only what the page shows. Off until the organizer turns it on. Separate from
calendar_links_enabledonGET /events/{slug}/settings/after-registering, which is the offer after booking, on the order page, the ticket and the emails. secret_redirect_urlstring- Where the event's page sends visitors while the event is
secret, as a 302 on the account's own addresses. Empty when there is none, and then a secret event's page is not found. Kept while the event is public, and only used while it is secret. Calendar entries link it in place of the event page while it is used. 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
201The new event.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.409The address is already an event's, a series' or a page's (slug_taken); the message names it.422The body was unreadable or carried an unknown field, or the name was blank (invalid_body); the slug is malformed, reserved or too long (invalid_slug); the currency is not one an event can be created in (invalid_currency); the timezone is not an IANA zone (invalid_timezone); a date is malformed, the end precedes the start, or a time is one the clocks skip in the event's timezone (invalid_dates).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Annual Conference 2027"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/events", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Annual Conference 2027"
}),
});
const data = await res.json();
{
"admin_url": "https://go.tito.io/youraccount/annual-conf/admin",
"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",
"ends_at": "2026-10-15T17:00:00Z",
"all_day": false,
"date_text": "",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"updated_at": "2026-03-09T16:04:12Z",
"draft": false,
"secret": false,
"unlisted": false,
"archived": false,
"archived_at": "2026-03-09T16:04:12Z",
"delete_on": "2026-11-10T14:30:00Z",
"anonymize_on": "2026-11-10T14:30:00Z",
"search_indexing": "inherit",
"no_index": false,
"share_title": "",
"share_text": "",
"share_image_url": "/a/8f2c1d9e4b7a6350.png",
"passed_params_on": true,
"passed_params_pattern": "utm_*",
"calendar_on_event_page": false,
"secret_redirect_url": "https://example.com/tickets",
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
Delete an event
DELETE /events/{slug}
Deletes an event that nothing records, and everything set up in it: its ticket types, pages, discounts, look and settings. Nothing records it means no order in any state, no ticket, no invitation, no ticket request, no quote, no waitlist entry, no invoice and no sent message. An event with any of those is refused with event_has_records and left exactly as it was. The account's owner can delete it from the admin, where it waits 30 days with registration closed and the owner and every admin are emailed (see delete_on), as long as it has never taken a payment. Cannot be undone. The event's address is free for a new event straight away. The account's owner only, so it needs team.manage. No request body.
Requires team.manage
Path parameters
slugstring required- The event's slug.
Response fields
Responses
200The event is deleted.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Something records the event (event_has_records); nothing was deleted. The owner deletes it from the admin instead, after 30 days.500Internal error.
curl -X DELETE 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: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"slug": "annual-conf",
"deleted": true
}
Get an event
GET /events/{slug}
Get an event by slug.
Requires events.view
Path parameters
slugstring required- The event's slug.
Response fields
admin_urlstring- Links to the event admin page, as an absolute URL: on the root domain, or on the instance's own address on a standalone account.
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 map the event page shows first; people can switch it themselves, and their browser remembers what they picked.
autois Apple Maps on Apple devices, OpenStreetMap on Linux and Google Maps elsewhere. A stored preference, not a promise about what people see: a service the page cannot draw ("apple" without Apple credentials, "osm" without a map pin) falls back to one it can, and an event with no map at all shows the address and its open-in links. - one of
autoapplegoogleosm ends_atstring (RFC 3339) or null- When the event ends, in the same form as
starts_at. Null when the event has no end time set. all_dayboolean- Whether the event's dates are shown without times. A date-only
starts_atsent to a create or a PATCH sets it. date_textstring- Free text the event page shows instead of the dates ("June 2027", "TBC"). Empty when the dates are shown.
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".
unlistedboolean- True when the event is unlisted: anyone with the link. The event is absent from the account's public event list and from its series page, and nothing else changes — its main page answers everybody, it sells, and it is indexable unless search_indexing says otherwise. A listing leaves it out; a link to it keeps working. Independent of secret; where both are true, secret is the stronger answer.
archivedboolean- True when the event has been archived (
POST /events/{slug}/archive). It is off the admin's everyday lists and the account's public event list and series pages, and its main page answers 404 to anyone not signed in, as a secret event's does. Orders, tickets and every link already sent keep working. A listing leaves it out. delete_onstring (RFC 3339) or null- When the event will be deleted, after the account's owner asked for it to be deleted in the admin. Null while it is not waiting to be deleted. An event with orders, tickets, invitations or ticket requests waits 30 days, and until then registration is closed: it is off the public lists, its main page answers 404 to anyone not signed in, and nothing can be bought or claimed.
POST /events/{slug}/keeptakes the request back and reopens registration. anonymize_onstring (RFC 3339) or null- When the attendee data on the event will be anonymized, after the account's owner asked for it in the admin. Null while it is not waiting to be. It waits 30 days and registration stays open meanwhile, so anyone who registers before then is anonymized too. On the day every buyer's, attendee's and invitee's details are removed, and the orders, what they cost and when they were paid stay.
POST /events/{slug}/keep-datatakes the request back until then. search_indexingstring- This event's OWN answer to whether search engines may list it. "inherit" means the account's search_indexing switch decides, which is where every event starts and most stay. "on" and "off" override it for this event only. Read no_index for the resolved answer.
- one of
inheritonoff no_indexboolean- The RESOLVED answer, and the one to read: true when this event is asking search engines not to list it, whether that came from its own switch or from the account's. It closes nothing and gates nothing — the page answers exactly as it did, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells — so do not read it as a visibility gate the way you read draft. It is independent of secret in both directions. Resolved rather than raw because the raw field alone gets the common case backwards: the account's search_indexing is off until somebody turns it on, so an event that has never touched its own switch is out of search while its own setting still says "inherit".
passed_params_onboolean- Whether the query params on a buyer's link are kept on the order they place. On by default. This is the switch and it is the only way off: writing an empty
passed_params_patternresets to the default rather than stopping capture. passed_params_patternstring- Which param names are kept, as comma-separated globs anchored at both ends.
utm_*keepsutm_source,utm_mediumand the rest. Click ids such asgclidandfbclidare not in the default and are added by name, because a click id identifies a person where a campaign name identifies a campaign. calendar_on_event_pageboolean- Whether the event page offers Add to calendar beside its date, before anybody books: Google Calendar, Outlook and a calendar file of the event's own date, carrying only what the page shows. Off until the organizer turns it on. Separate from
calendar_links_enabledonGET /events/{slug}/settings/after-registering, which is the offer after booking, on the order page, the ticket and the emails. secret_redirect_urlstring- Where the event's page sends visitors while the event is
secret, as a 302 on the account's own addresses. Empty when there is none, and then a secret event's page is not found. Kept while the event is public, and only used while it is secret. Calendar entries link it in place of the event page while it is used. 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();
{
"admin_url": "https://go.tito.io/youraccount/annual-conf/admin",
"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",
"ends_at": "2026-10-15T17:00:00Z",
"all_day": false,
"date_text": "",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"updated_at": "2026-03-09T16:04:12Z",
"draft": false,
"secret": false,
"unlisted": false,
"archived": false,
"archived_at": "2026-03-09T16:04:12Z",
"delete_on": "2026-11-10T14:30:00Z",
"anonymize_on": "2026-11-10T14:30:00Z",
"search_indexing": "inherit",
"no_index": false,
"share_title": "",
"share_text": "",
"share_image_url": "/a/8f2c1d9e4b7a6350.png",
"passed_params_on": true,
"passed_params_pattern": "utm_*",
"calendar_on_event_page": false,
"secret_redirect_url": "https://example.com/tickets",
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
Update an event's general settings
PATCH /events/{slug}
The General settings panel, one field at a time: absent fields are left unchanged, an explicit null is refused, and so is an unknown field. slug is the event's own address segment — inside a series the series carries the rest, and the internal slug is regenerated, so the answer's slug can differ from the URL's; the old address is retired and keeps redirecting. Changing venue_address drops the map pin, as a save the venue picker did not drive does. series is a series slug, or "" to take the event out of every series. starts_at and ends_at are 2006-01-02 (a date, which reads as all-day) or 2006-01-02T15:04 in the event's timezone; "" clears one. date_text is a free-text display override ("June 2027", "TBC") shown instead of the dates. A date left out is kept as it is. Currency is not here — it is fixed at creation. Gated by settings.manage, the panel's own capability, rather than events.manage.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Request body
slugstring- The event's own address segment. Inside a series the series carries the rest. A new address is lowercase letters, digits and hyphens. The address the event is already at is accepted as it is, so an imported one with a capital letter can be sent back unchanged.
map_providerstringauto(Apple Maps on Apple devices, OpenStreetMap on Linux, Google Maps elsewhere),apple,googleorosm(OpenStreetMap). The map the event page shows first.device, an earlier name forauto, is accepted and stored asauto.- one of
autoapplegoogleosm starts_atstring2006-01-02(all-day) or2006-01-02T15:04in the event's timezone, or RFC 3339 with an offset (the form the read returns);""for none.ends_atstring2006-01-02(all-day) or2006-01-02T15:04in the event's timezone, or RFC 3339 with an offset (the form the read returns);""for none.unlistedboolean- Off the listings, and nothing closes: anyone with the link can open the page and buy.
venue_latstring- Latitude as a decimal string from -90 to 90. Latitude and longitude must form a valid pair; empty strings clear the pin.
venue_lngstring- Longitude as a decimal string from -180 to 180. Latitude and longitude must form a valid pair; empty strings clear the pin.
passed_params_onboolean- Turn keeping a buyer's link parameters on or off. This is the only way off: writing an empty
passed_params_patternresets to the default instead. passed_params_patternstring- Comma-separated globs, anchored at both ends, lowercased. A wildcard needs at least two literal characters in front of it, so
*anda*are refused with 422. An empty string resets toutm_*. calendar_on_event_pageboolean- Turn Add to calendar on the event page, before anybody books, on or off. It does not change the offer after booking, which is
calendar_links_enabledon/events/{slug}/settings/after-registering. secret_redirect_urlstring- Where the event's page sends visitors while the event is secret. An
httpsaddress, or an empty string for none. Anything else is refused with 422. Kept when the event is made public.
Response fields
admin_urlstring- Links to the event admin page, as an absolute URL: on the root domain, or on the instance's own address on a standalone account.
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 map the event page shows first; people can switch it themselves, and their browser remembers what they picked.
autois Apple Maps on Apple devices, OpenStreetMap on Linux and Google Maps elsewhere. A stored preference, not a promise about what people see: a service the page cannot draw ("apple" without Apple credentials, "osm" without a map pin) falls back to one it can, and an event with no map at all shows the address and its open-in links. - one of
autoapplegoogleosm ends_atstring (RFC 3339) or null- When the event ends, in the same form as
starts_at. Null when the event has no end time set. all_dayboolean- Whether the event's dates are shown without times. A date-only
starts_atsent to a create or a PATCH sets it. date_textstring- Free text the event page shows instead of the dates ("June 2027", "TBC"). Empty when the dates are shown.
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".
unlistedboolean- True when the event is unlisted: anyone with the link. The event is absent from the account's public event list and from its series page, and nothing else changes — its main page answers everybody, it sells, and it is indexable unless search_indexing says otherwise. A listing leaves it out; a link to it keeps working. Independent of secret; where both are true, secret is the stronger answer.
archivedboolean- True when the event has been archived (
POST /events/{slug}/archive). It is off the admin's everyday lists and the account's public event list and series pages, and its main page answers 404 to anyone not signed in, as a secret event's does. Orders, tickets and every link already sent keep working. A listing leaves it out. delete_onstring (RFC 3339) or null- When the event will be deleted, after the account's owner asked for it to be deleted in the admin. Null while it is not waiting to be deleted. An event with orders, tickets, invitations or ticket requests waits 30 days, and until then registration is closed: it is off the public lists, its main page answers 404 to anyone not signed in, and nothing can be bought or claimed.
POST /events/{slug}/keeptakes the request back and reopens registration. anonymize_onstring (RFC 3339) or null- When the attendee data on the event will be anonymized, after the account's owner asked for it in the admin. Null while it is not waiting to be. It waits 30 days and registration stays open meanwhile, so anyone who registers before then is anonymized too. On the day every buyer's, attendee's and invitee's details are removed, and the orders, what they cost and when they were paid stay.
POST /events/{slug}/keep-datatakes the request back until then. search_indexingstring- This event's OWN answer to whether search engines may list it. "inherit" means the account's search_indexing switch decides, which is where every event starts and most stay. "on" and "off" override it for this event only. Read no_index for the resolved answer.
- one of
inheritonoff no_indexboolean- The RESOLVED answer, and the one to read: true when this event is asking search engines not to list it, whether that came from its own switch or from the account's. It closes nothing and gates nothing — the page answers exactly as it did, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells — so do not read it as a visibility gate the way you read draft. It is independent of secret in both directions. Resolved rather than raw because the raw field alone gets the common case backwards: the account's search_indexing is off until somebody turns it on, so an event that has never touched its own switch is out of search while its own setting still says "inherit".
passed_params_onboolean- Whether the query params on a buyer's link are kept on the order they place. On by default. This is the switch and it is the only way off: writing an empty
passed_params_patternresets to the default rather than stopping capture. passed_params_patternstring- Which param names are kept, as comma-separated globs anchored at both ends.
utm_*keepsutm_source,utm_mediumand the rest. Click ids such asgclidandfbclidare not in the default and are added by name, because a click id identifies a person where a campaign name identifies a campaign. calendar_on_event_pageboolean- Whether the event page offers Add to calendar beside its date, before anybody books: Google Calendar, Outlook and a calendar file of the event's own date, carrying only what the page shows. Off until the organizer turns it on. Separate from
calendar_links_enabledonGET /events/{slug}/settings/after-registering, which is the offer after booking, on the order page, the ticket and the emails. secret_redirect_urlstring- Where the event's page sends visitors while the event is
secret, as a 302 on the account's own addresses. Empty when there is none, and then a secret event's page is not found. Kept while the event is public, and only used while it is secret. Calendar entries link it in place of the event page while it is used. 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, in the same shapeGET /events/{slug}answers with.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The new address is already taken (slug_taken); nothing was written.422The body was unreadable, carried an unknown field or an explicit null, or blanked the name (invalid_body); a malformed slug (invalid_slug), timezone (invalid_timezone) or date (invalid_dates);map_providerother thanauto,apple,googleorosm(invalid_body); an unknown series (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Annual Conference 2027"}'
// 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: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Annual Conference 2027"
}),
});
const data = await res.json();
{
"admin_url": "https://go.tito.io/youraccount/annual-conf/admin",
"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",
"ends_at": "2026-10-15T17:00:00Z",
"all_day": false,
"date_text": "",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"updated_at": "2026-03-09T16:04:12Z",
"draft": false,
"secret": false,
"unlisted": false,
"archived": false,
"archived_at": "2026-03-09T16:04:12Z",
"delete_on": "2026-11-10T14:30:00Z",
"anonymize_on": "2026-11-10T14:30:00Z",
"search_indexing": "inherit",
"no_index": false,
"share_title": "",
"share_text": "",
"share_image_url": "/a/8f2c1d9e4b7a6350.png",
"passed_params_on": true,
"passed_params_pattern": "utm_*",
"calendar_on_event_page": false,
"secret_redirect_url": "https://example.com/tickets",
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
Archive an event
POST /events/{slug}/archive
Files the event away: it leaves the admin's everyday lists, the dashboard, and the account's public event list and series pages, and its main page answers 404 to anyone not signed in. Nothing is destroyed: orders, tickets, audience pages and every link already sent keep working. POST /events/{slug}/unarchive puts it back. No request body. Answers the event, with archived true.
Requires events.manage
Path parameters
slugstring required- The event's slug.
Response fields
admin_urlstring- Links to the event admin page, as an absolute URL: on the root domain, or on the instance's own address on a standalone account.
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 map the event page shows first; people can switch it themselves, and their browser remembers what they picked.
autois Apple Maps on Apple devices, OpenStreetMap on Linux and Google Maps elsewhere. A stored preference, not a promise about what people see: a service the page cannot draw ("apple" without Apple credentials, "osm" without a map pin) falls back to one it can, and an event with no map at all shows the address and its open-in links. - one of
autoapplegoogleosm ends_atstring (RFC 3339) or null- When the event ends, in the same form as
starts_at. Null when the event has no end time set. all_dayboolean- Whether the event's dates are shown without times. A date-only
starts_atsent to a create or a PATCH sets it. date_textstring- Free text the event page shows instead of the dates ("June 2027", "TBC"). Empty when the dates are shown.
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".
unlistedboolean- True when the event is unlisted: anyone with the link. The event is absent from the account's public event list and from its series page, and nothing else changes — its main page answers everybody, it sells, and it is indexable unless search_indexing says otherwise. A listing leaves it out; a link to it keeps working. Independent of secret; where both are true, secret is the stronger answer.
archivedboolean- True when the event has been archived (
POST /events/{slug}/archive). It is off the admin's everyday lists and the account's public event list and series pages, and its main page answers 404 to anyone not signed in, as a secret event's does. Orders, tickets and every link already sent keep working. A listing leaves it out. delete_onstring (RFC 3339) or null- When the event will be deleted, after the account's owner asked for it to be deleted in the admin. Null while it is not waiting to be deleted. An event with orders, tickets, invitations or ticket requests waits 30 days, and until then registration is closed: it is off the public lists, its main page answers 404 to anyone not signed in, and nothing can be bought or claimed.
POST /events/{slug}/keeptakes the request back and reopens registration. anonymize_onstring (RFC 3339) or null- When the attendee data on the event will be anonymized, after the account's owner asked for it in the admin. Null while it is not waiting to be. It waits 30 days and registration stays open meanwhile, so anyone who registers before then is anonymized too. On the day every buyer's, attendee's and invitee's details are removed, and the orders, what they cost and when they were paid stay.
POST /events/{slug}/keep-datatakes the request back until then. search_indexingstring- This event's OWN answer to whether search engines may list it. "inherit" means the account's search_indexing switch decides, which is where every event starts and most stay. "on" and "off" override it for this event only. Read no_index for the resolved answer.
- one of
inheritonoff no_indexboolean- The RESOLVED answer, and the one to read: true when this event is asking search engines not to list it, whether that came from its own switch or from the account's. It closes nothing and gates nothing — the page answers exactly as it did, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells — so do not read it as a visibility gate the way you read draft. It is independent of secret in both directions. Resolved rather than raw because the raw field alone gets the common case backwards: the account's search_indexing is off until somebody turns it on, so an event that has never touched its own switch is out of search while its own setting still says "inherit".
passed_params_onboolean- Whether the query params on a buyer's link are kept on the order they place. On by default. This is the switch and it is the only way off: writing an empty
passed_params_patternresets to the default rather than stopping capture. passed_params_patternstring- Which param names are kept, as comma-separated globs anchored at both ends.
utm_*keepsutm_source,utm_mediumand the rest. Click ids such asgclidandfbclidare not in the default and are added by name, because a click id identifies a person where a campaign name identifies a campaign. calendar_on_event_pageboolean- Whether the event page offers Add to calendar beside its date, before anybody books: Google Calendar, Outlook and a calendar file of the event's own date, carrying only what the page shows. Off until the organizer turns it on. Separate from
calendar_links_enabledonGET /events/{slug}/settings/after-registering, which is the offer after booking, on the order page, the ticket and the emails. secret_redirect_urlstring- Where the event's page sends visitors while the event is
secret, as a 302 on the account's own addresses. Empty when there is none, and then a secret event's page is not found. Kept while the event is public, and only used while it is secret. Calendar entries link it in place of the event page while it is used. 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 as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/archive \
-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/archive", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"admin_url": "https://go.tito.io/youraccount/annual-conf/admin",
"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",
"ends_at": "2026-10-15T17:00:00Z",
"all_day": false,
"date_text": "",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"updated_at": "2026-03-09T16:04:12Z",
"draft": false,
"secret": false,
"unlisted": false,
"archived": false,
"archived_at": "2026-03-09T16:04:12Z",
"delete_on": "2026-11-10T14:30:00Z",
"anonymize_on": "2026-11-10T14:30:00Z",
"search_indexing": "inherit",
"no_index": false,
"share_title": "",
"share_text": "",
"share_image_url": "/a/8f2c1d9e4b7a6350.png",
"passed_params_on": true,
"passed_params_pattern": "utm_*",
"calendar_on_event_page": false,
"secret_redirect_url": "https://example.com/tickets",
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
Duplicate an event
POST /events/{slug}/duplicate
The copy the Duplicate page makes: ticket types, offerings, questions, sections, design, settings and — unless switched off — coupons, portals, invitation lists, saved reports and downloads. The copy is born a DRAFT at a free address (slug, or one derived from name), in its source's series. starts_on and ends_on are dates (2006-01-02); the clock comes from the source, because nobody was asked about it. shift_dates moves every dated thing inside the event by the same number of weeks the event moved; the default copies them as they are. The page's per-row date table has no equivalent here — patch what needs moving afterwards. Answers the copy; POST /events/{slug}/publish makes it live.
Requires events.manage
Path parameters
slugstring required- The event's slug.
Request body
slugstring- The copy's own address segment; derived from the name when blank, shortened to 60 characters, and a generated address like
event-2when the name has no Latin spelling. The copy inherits its source's series. starts_onstring- The copy's start date,
2006-01-02; the clock comes from the source. Blank keeps the source's. shift_datesboolean- Move every dated thing inside the event by the weeks the event moved. Default false: copy them as they are.
Response fields
admin_urlstring- Links to the event admin page, as an absolute URL: on the root domain, or on the instance's own address on a standalone account.
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 map the event page shows first; people can switch it themselves, and their browser remembers what they picked.
autois Apple Maps on Apple devices, OpenStreetMap on Linux and Google Maps elsewhere. A stored preference, not a promise about what people see: a service the page cannot draw ("apple" without Apple credentials, "osm" without a map pin) falls back to one it can, and an event with no map at all shows the address and its open-in links. - one of
autoapplegoogleosm ends_atstring (RFC 3339) or null- When the event ends, in the same form as
starts_at. Null when the event has no end time set. all_dayboolean- Whether the event's dates are shown without times. A date-only
starts_atsent to a create or a PATCH sets it. date_textstring- Free text the event page shows instead of the dates ("June 2027", "TBC"). Empty when the dates are shown.
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".
unlistedboolean- True when the event is unlisted: anyone with the link. The event is absent from the account's public event list and from its series page, and nothing else changes — its main page answers everybody, it sells, and it is indexable unless search_indexing says otherwise. A listing leaves it out; a link to it keeps working. Independent of secret; where both are true, secret is the stronger answer.
archivedboolean- True when the event has been archived (
POST /events/{slug}/archive). It is off the admin's everyday lists and the account's public event list and series pages, and its main page answers 404 to anyone not signed in, as a secret event's does. Orders, tickets and every link already sent keep working. A listing leaves it out. delete_onstring (RFC 3339) or null- When the event will be deleted, after the account's owner asked for it to be deleted in the admin. Null while it is not waiting to be deleted. An event with orders, tickets, invitations or ticket requests waits 30 days, and until then registration is closed: it is off the public lists, its main page answers 404 to anyone not signed in, and nothing can be bought or claimed.
POST /events/{slug}/keeptakes the request back and reopens registration. anonymize_onstring (RFC 3339) or null- When the attendee data on the event will be anonymized, after the account's owner asked for it in the admin. Null while it is not waiting to be. It waits 30 days and registration stays open meanwhile, so anyone who registers before then is anonymized too. On the day every buyer's, attendee's and invitee's details are removed, and the orders, what they cost and when they were paid stay.
POST /events/{slug}/keep-datatakes the request back until then. search_indexingstring- This event's OWN answer to whether search engines may list it. "inherit" means the account's search_indexing switch decides, which is where every event starts and most stay. "on" and "off" override it for this event only. Read no_index for the resolved answer.
- one of
inheritonoff no_indexboolean- The RESOLVED answer, and the one to read: true when this event is asking search engines not to list it, whether that came from its own switch or from the account's. It closes nothing and gates nothing — the page answers exactly as it did, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells — so do not read it as a visibility gate the way you read draft. It is independent of secret in both directions. Resolved rather than raw because the raw field alone gets the common case backwards: the account's search_indexing is off until somebody turns it on, so an event that has never touched its own switch is out of search while its own setting still says "inherit".
passed_params_onboolean- Whether the query params on a buyer's link are kept on the order they place. On by default. This is the switch and it is the only way off: writing an empty
passed_params_patternresets to the default rather than stopping capture. passed_params_patternstring- Which param names are kept, as comma-separated globs anchored at both ends.
utm_*keepsutm_source,utm_mediumand the rest. Click ids such asgclidandfbclidare not in the default and are added by name, because a click id identifies a person where a campaign name identifies a campaign. calendar_on_event_pageboolean- Whether the event page offers Add to calendar beside its date, before anybody books: Google Calendar, Outlook and a calendar file of the event's own date, carrying only what the page shows. Off until the organizer turns it on. Separate from
calendar_links_enabledonGET /events/{slug}/settings/after-registering, which is the offer after booking, on the order page, the ticket and the emails. secret_redirect_urlstring- Where the event's page sends visitors while the event is
secret, as a 302 on the account's own addresses. Empty when there is none, and then a secret event's page is not found. Kept while the event is public, and only used while it is secret. Calendar entries link it in place of the event page while it is used. 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
201The copy, a draft.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The copy's address is already taken (slug_taken).422The body was unreadable, carried an unknown field, or the name was blank (invalid_body); a malformed slug (invalid_slug); a malformed date or an end before the start (invalid_dates).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/duplicate \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Annual Conference 2028"}'
// 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/duplicate", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Annual Conference 2028"
}),
});
const data = await res.json();
{
"admin_url": "https://go.tito.io/youraccount/annual-conf/admin",
"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",
"ends_at": "2026-10-15T17:00:00Z",
"all_day": false,
"date_text": "",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"updated_at": "2026-03-09T16:04:12Z",
"draft": false,
"secret": false,
"unlisted": false,
"archived": false,
"archived_at": "2026-03-09T16:04:12Z",
"delete_on": "2026-11-10T14:30:00Z",
"anonymize_on": "2026-11-10T14:30:00Z",
"search_indexing": "inherit",
"no_index": false,
"share_title": "",
"share_text": "",
"share_image_url": "/a/8f2c1d9e4b7a6350.png",
"passed_params_on": true,
"passed_params_pattern": "utm_*",
"calendar_on_event_page": false,
"secret_redirect_url": "https://example.com/tickets",
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
Keep an event waiting to be deleted
POST /events/{slug}/keep
Takes back a delete the account's owner asked for, so the event isn't deleted, and reopens registration exactly as it was. A no-op on an event that is not waiting to be deleted. No request body. Answers the event, with delete_on null.
Requires events.manage
Path parameters
slugstring required- The event's slug.
Response fields
admin_urlstring- Links to the event admin page, as an absolute URL: on the root domain, or on the instance's own address on a standalone account.
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 map the event page shows first; people can switch it themselves, and their browser remembers what they picked.
autois Apple Maps on Apple devices, OpenStreetMap on Linux and Google Maps elsewhere. A stored preference, not a promise about what people see: a service the page cannot draw ("apple" without Apple credentials, "osm" without a map pin) falls back to one it can, and an event with no map at all shows the address and its open-in links. - one of
autoapplegoogleosm ends_atstring (RFC 3339) or null- When the event ends, in the same form as
starts_at. Null when the event has no end time set. all_dayboolean- Whether the event's dates are shown without times. A date-only
starts_atsent to a create or a PATCH sets it. date_textstring- Free text the event page shows instead of the dates ("June 2027", "TBC"). Empty when the dates are shown.
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".
unlistedboolean- True when the event is unlisted: anyone with the link. The event is absent from the account's public event list and from its series page, and nothing else changes — its main page answers everybody, it sells, and it is indexable unless search_indexing says otherwise. A listing leaves it out; a link to it keeps working. Independent of secret; where both are true, secret is the stronger answer.
archivedboolean- True when the event has been archived (
POST /events/{slug}/archive). It is off the admin's everyday lists and the account's public event list and series pages, and its main page answers 404 to anyone not signed in, as a secret event's does. Orders, tickets and every link already sent keep working. A listing leaves it out. delete_onstring (RFC 3339) or null- When the event will be deleted, after the account's owner asked for it to be deleted in the admin. Null while it is not waiting to be deleted. An event with orders, tickets, invitations or ticket requests waits 30 days, and until then registration is closed: it is off the public lists, its main page answers 404 to anyone not signed in, and nothing can be bought or claimed.
POST /events/{slug}/keeptakes the request back and reopens registration. anonymize_onstring (RFC 3339) or null- When the attendee data on the event will be anonymized, after the account's owner asked for it in the admin. Null while it is not waiting to be. It waits 30 days and registration stays open meanwhile, so anyone who registers before then is anonymized too. On the day every buyer's, attendee's and invitee's details are removed, and the orders, what they cost and when they were paid stay.
POST /events/{slug}/keep-datatakes the request back until then. search_indexingstring- This event's OWN answer to whether search engines may list it. "inherit" means the account's search_indexing switch decides, which is where every event starts and most stay. "on" and "off" override it for this event only. Read no_index for the resolved answer.
- one of
inheritonoff no_indexboolean- The RESOLVED answer, and the one to read: true when this event is asking search engines not to list it, whether that came from its own switch or from the account's. It closes nothing and gates nothing — the page answers exactly as it did, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells — so do not read it as a visibility gate the way you read draft. It is independent of secret in both directions. Resolved rather than raw because the raw field alone gets the common case backwards: the account's search_indexing is off until somebody turns it on, so an event that has never touched its own switch is out of search while its own setting still says "inherit".
passed_params_onboolean- Whether the query params on a buyer's link are kept on the order they place. On by default. This is the switch and it is the only way off: writing an empty
passed_params_patternresets to the default rather than stopping capture. passed_params_patternstring- Which param names are kept, as comma-separated globs anchored at both ends.
utm_*keepsutm_source,utm_mediumand the rest. Click ids such asgclidandfbclidare not in the default and are added by name, because a click id identifies a person where a campaign name identifies a campaign. calendar_on_event_pageboolean- Whether the event page offers Add to calendar beside its date, before anybody books: Google Calendar, Outlook and a calendar file of the event's own date, carrying only what the page shows. Off until the organizer turns it on. Separate from
calendar_links_enabledonGET /events/{slug}/settings/after-registering, which is the offer after booking, on the order page, the ticket and the emails. secret_redirect_urlstring- Where the event's page sends visitors while the event is
secret, as a 302 on the account's own addresses. Empty when there is none, and then a secret event's page is not found. Kept while the event is public, and only used while it is secret. Calendar entries link it in place of the event page while it is used. 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 as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/keep \
-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/keep", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"admin_url": "https://go.tito.io/youraccount/annual-conf/admin",
"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",
"ends_at": "2026-10-15T17:00:00Z",
"all_day": false,
"date_text": "",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"updated_at": "2026-03-09T16:04:12Z",
"draft": false,
"secret": false,
"unlisted": false,
"archived": false,
"archived_at": "2026-03-09T16:04:12Z",
"delete_on": "2026-11-10T14:30:00Z",
"anonymize_on": "2026-11-10T14:30:00Z",
"search_indexing": "inherit",
"no_index": false,
"share_title": "",
"share_text": "",
"share_image_url": "/a/8f2c1d9e4b7a6350.png",
"passed_params_on": true,
"passed_params_pattern": "utm_*",
"calendar_on_event_page": false,
"secret_redirect_url": "https://example.com/tickets",
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
Keep the attendee data on an event waiting to be anonymized
POST /events/{slug}/keep-data
Takes back an anonymizing of the event's attendee data that the account's owner asked for in the admin, so nobody on it is anonymized. A no-op on an event that is not waiting to be anonymized. Asking for it is the admin's alone, because it emails the account's owner and every admin. No request body. Answers the event, with anonymize_on null.
Requires events.manage
Path parameters
slugstring required- The event's slug.
Response fields
admin_urlstring- Links to the event admin page, as an absolute URL: on the root domain, or on the instance's own address on a standalone account.
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 map the event page shows first; people can switch it themselves, and their browser remembers what they picked.
autois Apple Maps on Apple devices, OpenStreetMap on Linux and Google Maps elsewhere. A stored preference, not a promise about what people see: a service the page cannot draw ("apple" without Apple credentials, "osm" without a map pin) falls back to one it can, and an event with no map at all shows the address and its open-in links. - one of
autoapplegoogleosm ends_atstring (RFC 3339) or null- When the event ends, in the same form as
starts_at. Null when the event has no end time set. all_dayboolean- Whether the event's dates are shown without times. A date-only
starts_atsent to a create or a PATCH sets it. date_textstring- Free text the event page shows instead of the dates ("June 2027", "TBC"). Empty when the dates are shown.
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".
unlistedboolean- True when the event is unlisted: anyone with the link. The event is absent from the account's public event list and from its series page, and nothing else changes — its main page answers everybody, it sells, and it is indexable unless search_indexing says otherwise. A listing leaves it out; a link to it keeps working. Independent of secret; where both are true, secret is the stronger answer.
archivedboolean- True when the event has been archived (
POST /events/{slug}/archive). It is off the admin's everyday lists and the account's public event list and series pages, and its main page answers 404 to anyone not signed in, as a secret event's does. Orders, tickets and every link already sent keep working. A listing leaves it out. delete_onstring (RFC 3339) or null- When the event will be deleted, after the account's owner asked for it to be deleted in the admin. Null while it is not waiting to be deleted. An event with orders, tickets, invitations or ticket requests waits 30 days, and until then registration is closed: it is off the public lists, its main page answers 404 to anyone not signed in, and nothing can be bought or claimed.
POST /events/{slug}/keeptakes the request back and reopens registration. anonymize_onstring (RFC 3339) or null- When the attendee data on the event will be anonymized, after the account's owner asked for it in the admin. Null while it is not waiting to be. It waits 30 days and registration stays open meanwhile, so anyone who registers before then is anonymized too. On the day every buyer's, attendee's and invitee's details are removed, and the orders, what they cost and when they were paid stay.
POST /events/{slug}/keep-datatakes the request back until then. search_indexingstring- This event's OWN answer to whether search engines may list it. "inherit" means the account's search_indexing switch decides, which is where every event starts and most stay. "on" and "off" override it for this event only. Read no_index for the resolved answer.
- one of
inheritonoff no_indexboolean- The RESOLVED answer, and the one to read: true when this event is asking search engines not to list it, whether that came from its own switch or from the account's. It closes nothing and gates nothing — the page answers exactly as it did, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells — so do not read it as a visibility gate the way you read draft. It is independent of secret in both directions. Resolved rather than raw because the raw field alone gets the common case backwards: the account's search_indexing is off until somebody turns it on, so an event that has never touched its own switch is out of search while its own setting still says "inherit".
passed_params_onboolean- Whether the query params on a buyer's link are kept on the order they place. On by default. This is the switch and it is the only way off: writing an empty
passed_params_patternresets to the default rather than stopping capture. passed_params_patternstring- Which param names are kept, as comma-separated globs anchored at both ends.
utm_*keepsutm_source,utm_mediumand the rest. Click ids such asgclidandfbclidare not in the default and are added by name, because a click id identifies a person where a campaign name identifies a campaign. calendar_on_event_pageboolean- Whether the event page offers Add to calendar beside its date, before anybody books: Google Calendar, Outlook and a calendar file of the event's own date, carrying only what the page shows. Off until the organizer turns it on. Separate from
calendar_links_enabledonGET /events/{slug}/settings/after-registering, which is the offer after booking, on the order page, the ticket and the emails. secret_redirect_urlstring- Where the event's page sends visitors while the event is
secret, as a 302 on the account's own addresses. Empty when there is none, and then a secret event's page is not found. Kept while the event is public, and only used while it is secret. Calendar entries link it in place of the event page while it is used. 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 as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The day has come and anonymizing has begun, so some of the people have already gone (anonymizing_started).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/keep-data \
-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/keep-data", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"admin_url": "https://go.tito.io/youraccount/annual-conf/admin",
"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",
"ends_at": "2026-10-15T17:00:00Z",
"all_day": false,
"date_text": "",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"updated_at": "2026-03-09T16:04:12Z",
"draft": false,
"secret": false,
"unlisted": false,
"archived": false,
"archived_at": "2026-03-09T16:04:12Z",
"delete_on": "2026-11-10T14:30:00Z",
"anonymize_on": "2026-11-10T14:30:00Z",
"search_indexing": "inherit",
"no_index": false,
"share_title": "",
"share_text": "",
"share_image_url": "/a/8f2c1d9e4b7a6350.png",
"passed_params_on": true,
"passed_params_pattern": "utm_*",
"calendar_on_event_page": false,
"secret_redirect_url": "https://example.com/tickets",
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
Publish an event
POST /events/{slug}/publish
Ends a draft, and only that: no ticket type's on-sale state moves, so a publish never quietly starts selling. A duplicated event is born a draft; this is how it goes live. A no-op on an event that is not a draft. No request body. Answers the event.
Requires events.manage
Path parameters
slugstring required- The event's slug.
Response fields
admin_urlstring- Links to the event admin page, as an absolute URL: on the root domain, or on the instance's own address on a standalone account.
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 map the event page shows first; people can switch it themselves, and their browser remembers what they picked.
autois Apple Maps on Apple devices, OpenStreetMap on Linux and Google Maps elsewhere. A stored preference, not a promise about what people see: a service the page cannot draw ("apple" without Apple credentials, "osm" without a map pin) falls back to one it can, and an event with no map at all shows the address and its open-in links. - one of
autoapplegoogleosm ends_atstring (RFC 3339) or null- When the event ends, in the same form as
starts_at. Null when the event has no end time set. all_dayboolean- Whether the event's dates are shown without times. A date-only
starts_atsent to a create or a PATCH sets it. date_textstring- Free text the event page shows instead of the dates ("June 2027", "TBC"). Empty when the dates are shown.
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".
unlistedboolean- True when the event is unlisted: anyone with the link. The event is absent from the account's public event list and from its series page, and nothing else changes — its main page answers everybody, it sells, and it is indexable unless search_indexing says otherwise. A listing leaves it out; a link to it keeps working. Independent of secret; where both are true, secret is the stronger answer.
archivedboolean- True when the event has been archived (
POST /events/{slug}/archive). It is off the admin's everyday lists and the account's public event list and series pages, and its main page answers 404 to anyone not signed in, as a secret event's does. Orders, tickets and every link already sent keep working. A listing leaves it out. delete_onstring (RFC 3339) or null- When the event will be deleted, after the account's owner asked for it to be deleted in the admin. Null while it is not waiting to be deleted. An event with orders, tickets, invitations or ticket requests waits 30 days, and until then registration is closed: it is off the public lists, its main page answers 404 to anyone not signed in, and nothing can be bought or claimed.
POST /events/{slug}/keeptakes the request back and reopens registration. anonymize_onstring (RFC 3339) or null- When the attendee data on the event will be anonymized, after the account's owner asked for it in the admin. Null while it is not waiting to be. It waits 30 days and registration stays open meanwhile, so anyone who registers before then is anonymized too. On the day every buyer's, attendee's and invitee's details are removed, and the orders, what they cost and when they were paid stay.
POST /events/{slug}/keep-datatakes the request back until then. search_indexingstring- This event's OWN answer to whether search engines may list it. "inherit" means the account's search_indexing switch decides, which is where every event starts and most stay. "on" and "off" override it for this event only. Read no_index for the resolved answer.
- one of
inheritonoff no_indexboolean- The RESOLVED answer, and the one to read: true when this event is asking search engines not to list it, whether that came from its own switch or from the account's. It closes nothing and gates nothing — the page answers exactly as it did, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells — so do not read it as a visibility gate the way you read draft. It is independent of secret in both directions. Resolved rather than raw because the raw field alone gets the common case backwards: the account's search_indexing is off until somebody turns it on, so an event that has never touched its own switch is out of search while its own setting still says "inherit".
passed_params_onboolean- Whether the query params on a buyer's link are kept on the order they place. On by default. This is the switch and it is the only way off: writing an empty
passed_params_patternresets to the default rather than stopping capture. passed_params_patternstring- Which param names are kept, as comma-separated globs anchored at both ends.
utm_*keepsutm_source,utm_mediumand the rest. Click ids such asgclidandfbclidare not in the default and are added by name, because a click id identifies a person where a campaign name identifies a campaign. calendar_on_event_pageboolean- Whether the event page offers Add to calendar beside its date, before anybody books: Google Calendar, Outlook and a calendar file of the event's own date, carrying only what the page shows. Off until the organizer turns it on. Separate from
calendar_links_enabledonGET /events/{slug}/settings/after-registering, which is the offer after booking, on the order page, the ticket and the emails. secret_redirect_urlstring- Where the event's page sends visitors while the event is
secret, as a 302 on the account's own addresses. Empty when there is none, and then a secret event's page is not found. Kept while the event is public, and only used while it is secret. Calendar entries link it in place of the event page while it is used. 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 as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/publish \
-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/publish", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"admin_url": "https://go.tito.io/youraccount/annual-conf/admin",
"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",
"ends_at": "2026-10-15T17:00:00Z",
"all_day": false,
"date_text": "",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"updated_at": "2026-03-09T16:04:12Z",
"draft": false,
"secret": false,
"unlisted": false,
"archived": false,
"archived_at": "2026-03-09T16:04:12Z",
"delete_on": "2026-11-10T14:30:00Z",
"anonymize_on": "2026-11-10T14:30:00Z",
"search_indexing": "inherit",
"no_index": false,
"share_title": "",
"share_text": "",
"share_image_url": "/a/8f2c1d9e4b7a6350.png",
"passed_params_on": true,
"passed_params_pattern": "utm_*",
"calendar_on_event_page": false,
"secret_redirect_url": "https://example.com/tickets",
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
Restore an archived event
POST /events/{slug}/unarchive
The other direction of the archive toggle: the event is back on the admin's lists, in whichever view its dates put it, and back to whatever visibility it had before it was archived. No request body. Answers the event, with archived false.
Requires events.manage
Path parameters
slugstring required- The event's slug.
Response fields
admin_urlstring- Links to the event admin page, as an absolute URL: on the root domain, or on the instance's own address on a standalone account.
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 map the event page shows first; people can switch it themselves, and their browser remembers what they picked.
autois Apple Maps on Apple devices, OpenStreetMap on Linux and Google Maps elsewhere. A stored preference, not a promise about what people see: a service the page cannot draw ("apple" without Apple credentials, "osm" without a map pin) falls back to one it can, and an event with no map at all shows the address and its open-in links. - one of
autoapplegoogleosm ends_atstring (RFC 3339) or null- When the event ends, in the same form as
starts_at. Null when the event has no end time set. all_dayboolean- Whether the event's dates are shown without times. A date-only
starts_atsent to a create or a PATCH sets it. date_textstring- Free text the event page shows instead of the dates ("June 2027", "TBC"). Empty when the dates are shown.
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".
unlistedboolean- True when the event is unlisted: anyone with the link. The event is absent from the account's public event list and from its series page, and nothing else changes — its main page answers everybody, it sells, and it is indexable unless search_indexing says otherwise. A listing leaves it out; a link to it keeps working. Independent of secret; where both are true, secret is the stronger answer.
archivedboolean- True when the event has been archived (
POST /events/{slug}/archive). It is off the admin's everyday lists and the account's public event list and series pages, and its main page answers 404 to anyone not signed in, as a secret event's does. Orders, tickets and every link already sent keep working. A listing leaves it out. delete_onstring (RFC 3339) or null- When the event will be deleted, after the account's owner asked for it to be deleted in the admin. Null while it is not waiting to be deleted. An event with orders, tickets, invitations or ticket requests waits 30 days, and until then registration is closed: it is off the public lists, its main page answers 404 to anyone not signed in, and nothing can be bought or claimed.
POST /events/{slug}/keeptakes the request back and reopens registration. anonymize_onstring (RFC 3339) or null- When the attendee data on the event will be anonymized, after the account's owner asked for it in the admin. Null while it is not waiting to be. It waits 30 days and registration stays open meanwhile, so anyone who registers before then is anonymized too. On the day every buyer's, attendee's and invitee's details are removed, and the orders, what they cost and when they were paid stay.
POST /events/{slug}/keep-datatakes the request back until then. search_indexingstring- This event's OWN answer to whether search engines may list it. "inherit" means the account's search_indexing switch decides, which is where every event starts and most stay. "on" and "off" override it for this event only. Read no_index for the resolved answer.
- one of
inheritonoff no_indexboolean- The RESOLVED answer, and the one to read: true when this event is asking search engines not to list it, whether that came from its own switch or from the account's. It closes nothing and gates nothing — the page answers exactly as it did, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells — so do not read it as a visibility gate the way you read draft. It is independent of secret in both directions. Resolved rather than raw because the raw field alone gets the common case backwards: the account's search_indexing is off until somebody turns it on, so an event that has never touched its own switch is out of search while its own setting still says "inherit".
passed_params_onboolean- Whether the query params on a buyer's link are kept on the order they place. On by default. This is the switch and it is the only way off: writing an empty
passed_params_patternresets to the default rather than stopping capture. passed_params_patternstring- Which param names are kept, as comma-separated globs anchored at both ends.
utm_*keepsutm_source,utm_mediumand the rest. Click ids such asgclidandfbclidare not in the default and are added by name, because a click id identifies a person where a campaign name identifies a campaign. calendar_on_event_pageboolean- Whether the event page offers Add to calendar beside its date, before anybody books: Google Calendar, Outlook and a calendar file of the event's own date, carrying only what the page shows. Off until the organizer turns it on. Separate from
calendar_links_enabledonGET /events/{slug}/settings/after-registering, which is the offer after booking, on the order page, the ticket and the emails. secret_redirect_urlstring- Where the event's page sends visitors while the event is
secret, as a 302 on the account's own addresses. Empty when there is none, and then a secret event's page is not found. Kept while the event is public, and only used while it is secret. Calendar entries link it in place of the event page while it is used. 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 as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/unarchive \
-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/unarchive", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"admin_url": "https://go.tito.io/youraccount/annual-conf/admin",
"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",
"ends_at": "2026-10-15T17:00:00Z",
"all_day": false,
"date_text": "",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"updated_at": "2026-03-09T16:04:12Z",
"draft": false,
"secret": false,
"unlisted": false,
"archived": false,
"archived_at": "2026-03-09T16:04:12Z",
"delete_on": "2026-11-10T14:30:00Z",
"anonymize_on": "2026-11-10T14:30:00Z",
"search_indexing": "inherit",
"no_index": false,
"share_title": "",
"share_text": "",
"share_image_url": "/a/8f2c1d9e4b7a6350.png",
"passed_params_on": true,
"passed_params_pattern": "utm_*",
"calendar_on_event_page": false,
"secret_redirect_url": "https://example.com/tickets",
"series": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"fields": {},
"sections": [
{
"id": 1,
"name": "Tickets",
"position": 1
}
]
}
Series
List series
GET /series
Every series on the account, by name. A series groups events that are sold or shown together.
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"
}
]
}
Create a series
POST /series
A new grouping with a listing page of its own. listed and show_past default to true, as the form's boxes do; a blank slug derives from the name. The address is checked against every event, series and page on the account, and the refusal names what is in the way.
Requires events.manage
Request body
Response fields
slugstring- The address the series page answers on. Unique across this account's events, series and custom pages.
namestringlistedboolean- Whether the series appears on the account's own front page. It keeps its address either way.
created_atstring (RFC 3339) or null
Responses
201The new series.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.409The address already belongs to an event, a series or a page (slug_taken).422The body was unreadable or carried an unknown field, the name was blank or too long, or the intro too long (invalid_body); a malformed slug (invalid_slug).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/series \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Meet Tito"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/series", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Meet Tito"
}),
});
const data = await res.json();
{
"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"
}
Update a series
PATCH /series/{slug}
Absent fields are left unchanged; an explicit null and an unknown field are refused. Renaming the slug moves every event in the series — the series carries the first segment of their addresses — and the whole set is checked first, so a clash on any one event refuses the rename with nothing written.
Requires events.manage
Path parameters
slugstring required- The series' slug, as
GET /serieslists it.
Request body
Response fields
slugstring- The address the series page answers on. Unique across this account's events, series and custom pages.
namestringlistedboolean- Whether the series appears on the account's own front page. It keeps its address either way.
created_atstring (RFC 3339) or null
Responses
200The series as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The new address, or one of the event addresses under it, is already taken (slug_taken); the message names it.422The body was unreadable, carried an unknown field or an explicit null, blanked the name, or overran a length (invalid_body); a malformed slug (invalid_slug).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/series/annual-conf \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Meet Tito"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/series/annual-conf", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Meet Tito"
}),
});
const data = await res.json();
{
"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"
}
Ticket types
List an event's offerings
GET /events/{slug}/offerings
An offering is what an organiser sells beside a ticket: a GOOD (a thing, priced), a DONATION (which asks rather than charges) or a TIME (a session with runs beneath it, carried by a ticket rather than sold on its own). A good and a donation are products; a time is an entitlement — so each is addressed by a family and an id, good covering goods and donations, because an id alone does not say which it is. Goods and donations first, then times, then anything archived.
Requires catalog.view
Path parameters
slugstring required- The event's slug.
Response fields
offeringsarray of Offeringofferings[].familystring- The shape it is stored in, and the segment its own routes take.
- one of
goodsession offerings[].idintegerofferings[].namestringofferings[].price_centsinteger- A good's price; 0 on a donation, which asks rather than charges, and on a time, which a ticket carries.
offerings[].currencystringofferings[].stockinteger or null- The cap; null for no cap at all. A 0 is a real answer and reads as sold out.
offerings[].soldintegerofferings[].on_salebooleanofferings[].waitlistboolean- A session offering's waitlist switch: when one of its options is fully booked, people whose ticket includes the offering can wait for it, and a freed place is emailed to them in joining order. Present on a session only; absent on a good, a donation and an archived offering.
offerings[].waitlist_head_startinteger- A session offering's minutes between one person's offer email and the next: 15, 30, 60, 120, 360 or 1440. Present on a session only.
offerings[].descriptionstring- A good's or a donation's description, as PATCH takes it. Present on a good and a donation; absent on a session and an archived offering.
offerings[].tax_rate_idinteger- The tax rate a good or a donation charges, or 0 for no tax. Present on a good and a donation; absent on a session and an archived offering.
offerings[].price_includes_taxboolean- Whether
price_centsalready contains the tax. Present on a good and a donation; absent on a session and an archived offering. offerings[].allow_post_orderboolean- Whether a buyer may add it after ordering. Present on a good and a donation; absent on a session and an archived offering.
offerings[].assign_to_ticketboolean- The buyer is asked which ticket each one is for. Never true on a donation. Present on a good and a donation; absent on a session and an archived offering.
offerings[].sales_start_atstring (RFC 3339) or null- When it goes on sale, RFC 3339; null for no start. PATCH takes it back as it is. Present on a good and a donation; absent on a session and an archived offering.
offerings[].sales_end_atstring (RFC 3339) or null- When it comes off sale, RFC 3339; null for no end. Present on a good and a donation; absent on a session and an archived offering.
Responses
200The catalogue.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/offerings \
-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/offerings", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"offerings": [
{
"kind": "good",
"family": "good",
"id": 7,
"name": "T-shirt",
"price_cents": 2000,
"currency": "eur",
"donation_minimum_cents": 0,
"donation_suggested_cents": 0,
"stock": 50,
"available": 50,
"sold": 0,
"standalone": true,
"on_sale": true,
"archived": false,
"runs": 0,
"waitlist": false,
"waitlist_head_start": 60,
"description": "Organic cotton, unisex fit.",
"tax_rate_id": 0,
"price_includes_tax": false,
"allow_post_order": true,
"assign_to_ticket": false,
"sales_start_at": "2030-01-01T09:00:00Z",
"sales_end_at": "2030-02-01T09:00:00Z"
}
]
}
Add an offering
POST /events/{slug}/offerings
One of the three, said by kind. A good takes a price and a stock cap; a donation takes a floor and a suggestion and charges nothing; a time takes its first run's start, and an end an hour later when none is given. stock absent means no cap at all, while a typed 0 is a real answer and reads as sold out.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
Request body
namestring required
Response fields
idintegernamestringprice_centsinteger- A good's price; 0 on a donation, which asks rather than charges, and on a time, which a ticket carries.
currencystringsoldintegeron_salebooleanwaitlistboolean- A session offering's waitlist switch: when one of its options is fully booked, people whose ticket includes the offering can wait for it, and a freed place is emailed to them in joining order. Present on a session only; absent on a good, a donation and an archived offering.
waitlist_head_startinteger- A session offering's minutes between one person's offer email and the next: 15, 30, 60, 120, 360 or 1440. Present on a session only.
descriptionstring- A good's or a donation's description, as PATCH takes it. Present on a good and a donation; absent on a session and an archived offering.
tax_rate_idinteger- The tax rate a good or a donation charges, or 0 for no tax. Present on a good and a donation; absent on a session and an archived offering.
price_includes_taxboolean- Whether
price_centsalready contains the tax. Present on a good and a donation; absent on a session and an archived offering. allow_post_orderboolean- Whether a buyer may add it after ordering. Present on a good and a donation; absent on a session and an archived offering.
assign_to_ticketboolean- The buyer is asked which ticket each one is for. Never true on a donation. Present on a good and a donation; absent on a session and an archived offering.
sales_start_atstring (RFC 3339) or null- When it goes on sale, RFC 3339; null for no start. PATCH takes it back as it is. Present on a good and a donation; absent on a session and an archived offering.
sales_end_atstring (RFC 3339) or null- When it comes off sale, RFC 3339; null for no end. Present on a good and a donation; absent on a session and an archived offering.
Responses
201The new offering.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body was unreadable or carried an unknown field, the name was blank,kindwas not one of the three, a good carried a donation's amounts or the other way about, an amount was negative, a donation's suggestion sat below its floor, a time's start was unreadable or its end was not after it, orstandalonewas sent for a time — which is never standalone, because it reaches nobody until a ticket carries it (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/offerings \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"kind":"good","name":"T-shirt"}'
// 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/offerings", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"kind": "good",
"name": "T-shirt"
}),
});
const data = await res.json();
{
"kind": "good",
"family": "good",
"id": 7,
"name": "T-shirt",
"price_cents": 2000,
"currency": "eur",
"donation_minimum_cents": 0,
"donation_suggested_cents": 0,
"stock": 50,
"available": 50,
"sold": 0,
"standalone": true,
"on_sale": true,
"archived": false,
"runs": 0,
"waitlist": false,
"waitlist_head_start": 60,
"description": "Organic cotton, unisex fit.",
"tax_rate_id": 0,
"price_includes_tax": false,
"allow_post_order": true,
"assign_to_ticket": false,
"sales_start_at": "2030-01-01T09:00:00Z",
"sales_end_at": "2030-02-01T09:00:00Z"
}
Remove an offering
DELETE /events/{slug}/offerings/{family}/{id}
One verb doing one of two things, as the admin's own control does: an offering nothing has sold is DELETED, one with history behind it is ARCHIVED instead. The decision is made from the database rather than from what the caller says, so an offering somebody bought meanwhile cannot be destroyed. A delete answers {family, id, deleted: true}; an archive answers the offering, with archived true. For a deleted offering the history row is the only place its name survives, so it carries it.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
familystring required- The offering's family, as the catalogue reports it:
goodfor a good or a donation,sessionfor a time. idinteger required- The offering's id within its family.
Responses
200Deleted, or archived because it has history behind it.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/offerings/{family}/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/offerings/{family}/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
Get an offering
GET /events/{slug}/offerings/{family}/{id}
One offering, with its stock and what is left of it.
Requires catalog.view
Path parameters
slugstring required- The event's slug.
familystring required- The offering's family, as the catalogue reports it:
goodfor a good or a donation,sessionfor a time. idinteger required- The offering's id within its family.
Response fields
idintegernamestringprice_centsinteger- A good's price; 0 on a donation, which asks rather than charges, and on a time, which a ticket carries.
currencystringsoldintegeron_salebooleanwaitlistboolean- A session offering's waitlist switch: when one of its options is fully booked, people whose ticket includes the offering can wait for it, and a freed place is emailed to them in joining order. Present on a session only; absent on a good, a donation and an archived offering.
waitlist_head_startinteger- A session offering's minutes between one person's offer email and the next: 15, 30, 60, 120, 360 or 1440. Present on a session only.
descriptionstring- A good's or a donation's description, as PATCH takes it. Present on a good and a donation; absent on a session and an archived offering.
tax_rate_idinteger- The tax rate a good or a donation charges, or 0 for no tax. Present on a good and a donation; absent on a session and an archived offering.
price_includes_taxboolean- Whether
price_centsalready contains the tax. Present on a good and a donation; absent on a session and an archived offering. allow_post_orderboolean- Whether a buyer may add it after ordering. Present on a good and a donation; absent on a session and an archived offering.
assign_to_ticketboolean- The buyer is asked which ticket each one is for. Never true on a donation. Present on a good and a donation; absent on a session and an archived offering.
sales_start_atstring (RFC 3339) or null- When it goes on sale, RFC 3339; null for no start. PATCH takes it back as it is. Present on a good and a donation; absent on a session and an archived offering.
sales_end_atstring (RFC 3339) or null- When it comes off sale, RFC 3339; null for no end. Present on a good and a donation; absent on a session and an archived offering.
Responses
200The offering.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/offerings/{family}/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/offerings/{family}/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"kind": "good",
"family": "good",
"id": 7,
"name": "T-shirt",
"price_cents": 2000,
"currency": "eur",
"donation_minimum_cents": 0,
"donation_suggested_cents": 0,
"stock": 50,
"available": 50,
"sold": 0,
"standalone": true,
"on_sale": true,
"archived": false,
"runs": 0,
"waitlist": false,
"waitlist_head_start": 60,
"description": "Organic cotton, unisex fit.",
"tax_rate_id": 0,
"price_includes_tax": false,
"allow_post_order": true,
"assign_to_ticket": false,
"sales_start_at": "2030-01-01T09:00:00Z",
"sales_end_at": "2030-02-01T09:00:00Z"
}
Update an offering
PATCH /events/{slug}/offerings/{family}/{id}
The offering's own fields. Absent means unchanged, an explicit null on a field that is not nullable and an unknown field are refused — so a body naming one field touches one field. (The admin's editor is two forms and must name which posted, because a form that does not render a checkbox does not post one and a whole-row save would clear what it never drew; a caller naming fields has no such problem, which is why there is no tab here.) stock null is no cap at all and 0 is sold out. A donation's two amounts are checked against whichever half is already in force, not only against what was sent. A donation is never priced and never assigned to a ticket — it is money rather than a thing somebody receives. A time's own fields are its runs, which are rows of their own and not served here yet; the one thing a time takes here is its waitlist (waitlist, waitlist_head_start), and a body naming anything else on a time is refused.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
familystring required- The offering's family, as the catalogue reports it:
goodfor a good or a donation,sessionfor a time. idinteger required- The offering's id within its family.
Request body
namestringdescriptionstringassign_to_ticketboolean- The buyer is asked which ticket each one is for. Never true on a donation.
sales_start_atstring or null- When it goes on sale: on the event's clock (
2030-01-01T09:00), or RFC 3339 as the read answers; "" or null for no start. sales_end_atstring or null- When it comes off: on the event's clock, or RFC 3339 as the read answers; "" or null for no end.
waitlistboolean- A session offering's waitlist switch. Switching it on is refused unless the offering is shaped as options. Switching it off keeps everybody already waiting. Refused on a good or a donation.
waitlist_head_startinteger- A session offering's minutes between offer emails: 15, 30, 60, 120, 360 or 1440. Refused on a good or a donation.
Response fields
idintegernamestringprice_centsinteger- A good's price; 0 on a donation, which asks rather than charges, and on a time, which a ticket carries.
currencystringsoldintegeron_salebooleanwaitlistboolean- A session offering's waitlist switch: when one of its options is fully booked, people whose ticket includes the offering can wait for it, and a freed place is emailed to them in joining order. Present on a session only; absent on a good, a donation and an archived offering.
waitlist_head_startinteger- A session offering's minutes between one person's offer email and the next: 15, 30, 60, 120, 360 or 1440. Present on a session only.
descriptionstring- A good's or a donation's description, as PATCH takes it. Present on a good and a donation; absent on a session and an archived offering.
tax_rate_idinteger- The tax rate a good or a donation charges, or 0 for no tax. Present on a good and a donation; absent on a session and an archived offering.
price_includes_taxboolean- Whether
price_centsalready contains the tax. Present on a good and a donation; absent on a session and an archived offering. allow_post_orderboolean- Whether a buyer may add it after ordering. Present on a good and a donation; absent on a session and an archived offering.
assign_to_ticketboolean- The buyer is asked which ticket each one is for. Never true on a donation. Present on a good and a donation; absent on a session and an archived offering.
sales_start_atstring (RFC 3339) or null- When it goes on sale, RFC 3339; null for no start. PATCH takes it back as it is. Present on a good and a donation; absent on a session and an archived offering.
sales_end_atstring (RFC 3339) or null- When it comes off sale, RFC 3339; null for no end. Present on a good and a donation; absent on a session and an archived offering.
Responses
200The offering as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The price would leave a bundle carrying this good discounted below nothing, so the old price stands (bundle_would_free, naming the bundle); or the offering is archived and must be restored before it is edited (archived).422The body was unreadable or carried an unknown field, the name was blanked, a price or amount was negative, a donation was priced or assigned to a ticket, a good was given a donation's amounts, the tax rate is not this account's, a sale-window time was unreadable, the window closes before it opens, a time was sent anything but its waitlist, a waitlist was sent to a good or a donation, a head start is not one of the choices, or a waitlist was switched on for a time not shaped as options (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/offerings/{family}/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Event t-shirt"}'
// 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/offerings/{family}/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Event t-shirt"
}),
});
const data = await res.json();
{
"kind": "good",
"family": "good",
"id": 7,
"name": "T-shirt",
"price_cents": 2000,
"currency": "eur",
"donation_minimum_cents": 0,
"donation_suggested_cents": 0,
"stock": 50,
"available": 50,
"sold": 0,
"standalone": true,
"on_sale": true,
"archived": false,
"runs": 0,
"waitlist": false,
"waitlist_head_start": 60,
"description": "Organic cotton, unisex fit.",
"tax_rate_id": 0,
"price_includes_tax": false,
"allow_post_order": true,
"assign_to_ticket": false,
"sales_start_at": "2030-01-01T09:00:00Z",
"sales_end_at": "2030-02-01T09:00:00Z"
}
Take an offering off sale
POST /events/{slug}/offerings/{family}/{id}/off-sale
As above, the other way. A TIME's master switch closes everything beneath it and is a different write; it is not served here yet.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
familystring required- The offering's family, as the catalogue reports it:
goodfor a good or a donation,sessionfor a time. idinteger required- The offering's id within its family.
Response fields
idintegernamestringprice_centsinteger- A good's price; 0 on a donation, which asks rather than charges, and on a time, which a ticket carries.
currencystringsoldintegeron_salebooleanwaitlistboolean- A session offering's waitlist switch: when one of its options is fully booked, people whose ticket includes the offering can wait for it, and a freed place is emailed to them in joining order. Present on a session only; absent on a good, a donation and an archived offering.
waitlist_head_startinteger- A session offering's minutes between one person's offer email and the next: 15, 30, 60, 120, 360 or 1440. Present on a session only.
descriptionstring- A good's or a donation's description, as PATCH takes it. Present on a good and a donation; absent on a session and an archived offering.
tax_rate_idinteger- The tax rate a good or a donation charges, or 0 for no tax. Present on a good and a donation; absent on a session and an archived offering.
price_includes_taxboolean- Whether
price_centsalready contains the tax. Present on a good and a donation; absent on a session and an archived offering. allow_post_orderboolean- Whether a buyer may add it after ordering. Present on a good and a donation; absent on a session and an archived offering.
assign_to_ticketboolean- The buyer is asked which ticket each one is for. Never true on a donation. Present on a good and a donation; absent on a session and an archived offering.
sales_start_atstring (RFC 3339) or null- When it goes on sale, RFC 3339; null for no start. PATCH takes it back as it is. Present on a good and a donation; absent on a session and an archived offering.
sales_end_atstring (RFC 3339) or null- When it comes off sale, RFC 3339; null for no end. Present on a good and a donation; absent on a session and an archived offering.
Responses
200The offering as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422A time's switch is not served yet (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/offerings/{family}/1/off-sale \
-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/offerings/{family}/1/off-sale", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"kind": "good",
"family": "good",
"id": 7,
"name": "T-shirt",
"price_cents": 2000,
"currency": "eur",
"donation_minimum_cents": 0,
"donation_suggested_cents": 0,
"stock": 50,
"available": 50,
"sold": 0,
"standalone": true,
"on_sale": true,
"archived": false,
"runs": 0,
"waitlist": false,
"waitlist_head_start": 60,
"description": "Organic cotton, unisex fit.",
"tax_rate_id": 0,
"price_includes_tax": false,
"allow_post_order": true,
"assign_to_ticket": false,
"sales_start_at": "2030-01-01T09:00:00Z",
"sales_end_at": "2030-02-01T09:00:00Z"
}
Put an offering on sale
POST /events/{slug}/offerings/{family}/{id}/on-sale
On sale is STATE rather than a setting, which is why it has its own verb: an edit form carrying it would take a good off sale on every save, because an unrendered checkbox posts nothing and reads exactly like an unticked one. A repeat of the state already held records nothing.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
familystring required- The offering's family, as the catalogue reports it:
goodfor a good or a donation,sessionfor a time. idinteger required- The offering's id within its family.
Response fields
idintegernamestringprice_centsinteger- A good's price; 0 on a donation, which asks rather than charges, and on a time, which a ticket carries.
currencystringsoldintegeron_salebooleanwaitlistboolean- A session offering's waitlist switch: when one of its options is fully booked, people whose ticket includes the offering can wait for it, and a freed place is emailed to them in joining order. Present on a session only; absent on a good, a donation and an archived offering.
waitlist_head_startinteger- A session offering's minutes between one person's offer email and the next: 15, 30, 60, 120, 360 or 1440. Present on a session only.
descriptionstring- A good's or a donation's description, as PATCH takes it. Present on a good and a donation; absent on a session and an archived offering.
tax_rate_idinteger- The tax rate a good or a donation charges, or 0 for no tax. Present on a good and a donation; absent on a session and an archived offering.
price_includes_taxboolean- Whether
price_centsalready contains the tax. Present on a good and a donation; absent on a session and an archived offering. allow_post_orderboolean- Whether a buyer may add it after ordering. Present on a good and a donation; absent on a session and an archived offering.
assign_to_ticketboolean- The buyer is asked which ticket each one is for. Never true on a donation. Present on a good and a donation; absent on a session and an archived offering.
sales_start_atstring (RFC 3339) or null- When it goes on sale, RFC 3339; null for no start. PATCH takes it back as it is. Present on a good and a donation; absent on a session and an archived offering.
sales_end_atstring (RFC 3339) or null- When it comes off sale, RFC 3339; null for no end. Present on a good and a donation; absent on a session and an archived offering.
Responses
200The offering as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/offerings/{family}/1/on-sale \
-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/offerings/{family}/1/on-sale", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"kind": "good",
"family": "good",
"id": 7,
"name": "T-shirt",
"price_cents": 2000,
"currency": "eur",
"donation_minimum_cents": 0,
"donation_suggested_cents": 0,
"stock": 50,
"available": 50,
"sold": 0,
"standalone": true,
"on_sale": true,
"archived": false,
"runs": 0,
"waitlist": false,
"waitlist_head_start": 60,
"description": "Organic cotton, unisex fit.",
"tax_rate_id": 0,
"price_includes_tax": false,
"allow_post_order": true,
"assign_to_ticket": false,
"sales_start_at": "2030-01-01T09:00:00Z",
"sales_end_at": "2030-02-01T09:00:00Z"
}
Restore an archived offering
POST /events/{slug}/offerings/{family}/{id}/restore
Back into the catalogue, UNCONNECTED and SWITCHED OFF whichever sort it is — a restored offering never quietly starts selling again.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
familystring required- The offering's family, as the catalogue reports it:
goodfor a good or a donation,sessionfor a time. idinteger required- The offering's id within its family.
Response fields
idintegernamestringprice_centsinteger- A good's price; 0 on a donation, which asks rather than charges, and on a time, which a ticket carries.
currencystringsoldintegeron_salebooleanwaitlistboolean- A session offering's waitlist switch: when one of its options is fully booked, people whose ticket includes the offering can wait for it, and a freed place is emailed to them in joining order. Present on a session only; absent on a good, a donation and an archived offering.
waitlist_head_startinteger- A session offering's minutes between one person's offer email and the next: 15, 30, 60, 120, 360 or 1440. Present on a session only.
descriptionstring- A good's or a donation's description, as PATCH takes it. Present on a good and a donation; absent on a session and an archived offering.
tax_rate_idinteger- The tax rate a good or a donation charges, or 0 for no tax. Present on a good and a donation; absent on a session and an archived offering.
price_includes_taxboolean- Whether
price_centsalready contains the tax. Present on a good and a donation; absent on a session and an archived offering. allow_post_orderboolean- Whether a buyer may add it after ordering. Present on a good and a donation; absent on a session and an archived offering.
assign_to_ticketboolean- The buyer is asked which ticket each one is for. Never true on a donation. Present on a good and a donation; absent on a session and an archived offering.
sales_start_atstring (RFC 3339) or null- When it goes on sale, RFC 3339; null for no start. PATCH takes it back as it is. Present on a good and a donation; absent on a session and an archived offering.
sales_end_atstring (RFC 3339) or null- When it comes off sale, RFC 3339; null for no end. Present on a good and a donation; absent on a session and an archived offering.
Responses
200The offering as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The offering is in the catalogue already (not_archived).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/offerings/{family}/1/restore \
-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/offerings/{family}/1/restore", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"kind": "good",
"family": "good",
"id": 7,
"name": "T-shirt",
"price_cents": 2000,
"currency": "eur",
"donation_minimum_cents": 0,
"donation_suggested_cents": 0,
"stock": 50,
"available": 50,
"sold": 0,
"standalone": true,
"on_sale": true,
"archived": false,
"runs": 0,
"waitlist": false,
"waitlist_head_start": 60,
"description": "Organic cotton, unisex fit.",
"tax_rate_id": 0,
"price_includes_tax": false,
"allow_post_order": true,
"assign_to_ticket": false,
"sales_start_at": "2030-01-01T09:00:00Z",
"sales_end_at": "2030-02-01T09:00:00Z"
}
List ticket types
GET /events/{slug}/ticket-types
List an event's ticket types. Archived ticket types are not included.
Requires catalog.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[].hide_freeboolean- Whether the event page and the widget leave the word Free off this ticket. Only ever applies to a free ticket: a ticket with a price always shows it.
ticket_types[].signup_hide_nameboolean- Whether a free event's sign-up form leaves this ticket's name and price off, showing only the questions and Confirm.
ticket_types[].pay_what_you_wantboolean- Whether buyers choose this ticket's price. When true, price_cents and prices are the least a buyer can pay, and suggested_price_cents is what the price box on the event page and the widget starts on. Surfaces with no price box (invitations, waitlist offers, the API's own checkout) charge price_cents.
ticket_types[].suggested_price_centsinteger- The price a pay-what-you-want ticket's box starts on, in minor units of the event's currency. 0 means no suggestion, and it is always 0 when pay_what_you_want is false.
ticket_types[].show_left_atinteger- How few of this ticket have to be left before the event page and the widget say how many. 0 is never, and is the default.
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[].descriptionstring- Organizer text shown under the ticket's name. Empty when there is none. Plain text, or a composer body (the stored form of a message body) when it carries bold, italic, links or bullets. Send either back as it is. description_text and description_html are this read for a person.
ticket_types[].description_textstring- The description as plain lines: a bullet starts with a bullet character and a link is its words followed by its address in brackets. The same as description when nothing is formatted.
ticket_types[].description_htmlstring- The description as HTML, the way a buyer page draws it: escaped text whose line breaks are newlines, with strong, em, links that open in a new tab, and ul lists. Escaped plain text when nothing is formatted.
ticket_types[].tax_rate_idinteger- The tax rate this ticket charges (
GET /tax-rates), or 0 for no tax. The same name the create and PATCH bodies take. ticket_types[].price_includes_taxboolean- Whether
price_centsalready contains the tax. Always false whentax_rate_idis 0. ticket_types[].second_tax_rate_idinteger- A second tax rate this ticket charges beside
tax_rate_id, or 0 for none. Each rate is charged on the price, never on the other's tax (Canada's GST and QST). ticket_types[].reverse_chargestring- Who pays no VAT on this ticket:
""for nobody,countryfor companies inreverse_charge_country, oreufor companies in EU countries other than the account's own. A company qualifies only once the EU register confirms its VAT number while it books. ticket_types[].reverse_charge_countrystring- The country whose companies get reverse charge, as an ISO 3166 code, when
reverse_chargeiscountry. Empty otherwise. ticket_types[].reverse_charge_notestring- The organizer's own wording for the invoice of a reverse-charged order. Empty for the standard sentence.
ticket_types[].colorstring- The ticket's color in the admin's lists: one of
slate,red,amber,green,teal,blue,indigoorpink, or empty for none. ticket_types[].max_per_orderinteger- The most of this ticket one order may hold. 0 follows the event's own limit.
ticket_types[].allow_downgradeboolean- Whether this type is ticked under the event's
allow_downgrades. A buyer may change a ticket of this type down to a cheaper one, and get the difference back, when both are on. The same name PATCH takes. ticket_types[].allow_upgradeboolean- Whether this type is ticked under the event's
allow_upgrades. A buyer may change a ticket of this type up to a dearer one, and pay the difference, when both are on. The same name PATCH takes. ticket_types[].holder_changes_modestringinheritthe event's rule, orallow/denyholder changes on this type.- one of
inheritallowdeny ticket_types[].holder_changes_untilstring- The deadline for holder changes when the mode is allow, as the form stores it; empty for none, and always empty while the mode is inherit.
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- Tickets issued at this rung: seats on paid orders, on unpaid orders whose tickets already exist, and added to an order afterwards. 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 first companion — the whole rule when it names one.
ticket_types[].companion.ticket_type_idsarray of integer- Every companion ticket type, any of which counts. They count together: one of each covers two.
ticket_types[].companion.quantityinteger- How many companions are needed per
per_quantityof this type. ticket_types[].companion.per_quantityinteger- How many of this type each set of companions covers.
0is once per order, however many of this type the order holds. ticket_types[].companion.strictnessstringrecommendedshows people a tip and never blocks;requiredrefuses a checkout that falls short — unless no companion can be bought, in which case the rule pauses so this type stays sellable.- one of
recommendedrequired ticket_types[].legacy_slugstring- The Tito Pro ticket type's slug, which a website moving from Tito Pro names in
data-tito-q-<slug>to preselect a quantity. Empty for a type made in Tito Go. Carried over by the account import. ticket_types[].waitlistboolean- Whether the type opens a waitlist when it is fully booked. The people on it are at GET /events/{slug}/waitlist.
ticket_types[].waitlist_head_startinteger- Minutes between one person on the waitlist being emailed about a free place and the next person. 60 unless changed. Only used while
waitlist_modeis first_come. - one of
1530601203601440 ticket_types[].waitlist_modestring- What happens when a place becomes free. first_come emails everyone waiting, in the order they joined, a head start apart, and whoever takes the place first gets it. by_hand emails nobody: the free places show on the Waitlist screen and are offered by hand. first_come unless changed.
- one of
first_comeby_hand ticket_types[].availableinteger or null- How many a buyer can still buy: the cap less what is sold and held, narrowed by the price ladder, less the places held for the waitlist. The figure the event page shows and an order created through this API is held to. null when the type is uncapped.
ticket_types[].waitlist_heldinteger- How many free places are kept off sale for the people on the waitlist: the smaller of the number waiting (or offered a place) and the number free. 0 for a type with no waitlist, a bundle, or a type whose time is picked from a timetable.
ticket_types[].sales_start_atstring (RFC 3339) or null- When the type goes on sale, RFC 3339; null for no start. Applies only while
on_saleis true: before it the type is listed andavailableis 0. PATCH takes it back as it is. ticket_types[].sales_end_atstring (RFC 3339) or null- When the type comes off sale, RFC 3339; null for no end. The end is exclusive, and from it the type is listed with
available0.
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,
"hide_free": false,
"signup_hide_name": false,
"pay_what_you_want": false,
"suggested_price_cents": 0,
"show_left_at": 0,
"sold": 142,
"description": "Includes lunch on both days.",
"description_text": "Includes lunch on both days.",
"description_html": "Includes \u003cstrong\u003elunch\u003c/strong\u003e on both days.",
"tax_rate_id": 0,
"price_includes_tax": false,
"second_tax_rate_id": 0,
"reverse_charge": "",
"reverse_charge_country": "",
"reverse_charge_note": "",
"color": "teal",
"max_per_order": 0,
"allow_downgrade": false,
"allow_upgrade": true,
"holder_changes_mode": "inherit",
"holder_changes_until": "",
"anonymous": false,
"qr_enabled": true,
"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,
"ticket_type_ids": [
2,
3
],
"quantity": 1,
"per_quantity": 1,
"strictness": "recommended"
},
"legacy_slug": "",
"waitlist": false,
"waitlist_head_start": 60,
"waitlist_mode": "first_come",
"available": 12,
"waitlist_held": 0,
"sales_start_at": "2027-01-01T12:00:00Z",
"sales_end_at": "2027-02-19T12:00:00Z"
}
]
}
Create a ticket type
POST /events/{slug}/ticket-types
The "Add a ticket type" form's Basics. price_cents is in the event's currency (default 0, free); quantity is the capacity — absent or null is uncapped, 0 is none available. prices sets the price in each extra currency the event sells in; a currency left out is one the type is not offered in. The type is born ON sale, as the form's is; off-sale takes it down. Advanced settings are a PATCH after creation, as they are a second tab on the form. This writes what a SINGLE-PRICED ticket has: name, price, capacity, description, the tax decision, the Advanced switches, and the price in each extra currency. A type priced by a ladder, by volume bands or as a bundle takes its price and capacity from rows of its own, written at /events/{slug}/ticket-types/{id}/tiers, /events/{slug}/ticket-types/{id}/bands and /events/{slug}/ticket-types/{id}/bundle, so on one of those the price and capacity fields here are refused (pricing_mode) and everything else still saves. The companion and free-ticket rules have endpoints of their own, /events/{slug}/ticket-types/{id}/companion and /events/{slug}/ticket-types/{id}/free-with. The ticket's own time is admin-only. Ticket amounts are capped at 99,999,999 whole currency units plus the currency’s fractional minor units, matching the UI.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
Request body
descriptionstring- Shown under the name on the event page. Plain text, or a composer body carrying bold, italic, links (https or mailto) and bullets. Anything else in a body is kept as its words.
price_includes_taxboolean- Whether
price_centsalready includes the tax. Meaningless, and stored false, with no rate. second_tax_rate_idinteger- A second of the account's tax rates, charged beside
tax_rate_idon the same price, or 0 for none. Needs atax_rate_id, and may not be the same rate. Default 0. pay_what_you_wantboolean- Let buyers choose the price. price_cents is then the minimum, and may be 0 to let people come for free.
suggested_price_centsinteger- With pay_what_you_want, the price the buyer's box starts on, 0 for none. Never less than price_cents (422).
pricesobject- Price per currency, lowercase code → cents. The event's own currency, if present, must equal
price_cents. On a PATCH,nullclears a currency (the type is then not offered there) and an absent key is unchanged.
Response fields
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.
hide_freeboolean- Whether the event page and the widget leave the word Free off this ticket. Only ever applies to a free ticket: a ticket with a price always shows it.
signup_hide_nameboolean- Whether a free event's sign-up form leaves this ticket's name and price off, showing only the questions and Confirm.
pay_what_you_wantboolean- Whether buyers choose this ticket's price. When true, price_cents and prices are the least a buyer can pay, and suggested_price_cents is what the price box on the event page and the widget starts on. Surfaces with no price box (invitations, waitlist offers, the API's own checkout) charge price_cents.
suggested_price_centsinteger- The price a pay-what-you-want ticket's box starts on, in minor units of the event's currency. 0 means no suggestion, and it is always 0 when pay_what_you_want is false.
show_left_atinteger- How few of this ticket have to be left before the event page and the widget say how many. 0 is never, and is the default.
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.
descriptionstring- Organizer text shown under the ticket's name. Empty when there is none. Plain text, or a composer body (the stored form of a message body) when it carries bold, italic, links or bullets. Send either back as it is. description_text and description_html are this read for a person.
description_textstring- The description as plain lines: a bullet starts with a bullet character and a link is its words followed by its address in brackets. The same as description when nothing is formatted.
description_htmlstring- The description as HTML, the way a buyer page draws it: escaped text whose line breaks are newlines, with strong, em, links that open in a new tab, and ul lists. Escaped plain text when nothing is formatted.
tax_rate_idinteger- The tax rate this ticket charges (
GET /tax-rates), or 0 for no tax. The same name the create and PATCH bodies take. price_includes_taxboolean- Whether
price_centsalready contains the tax. Always false whentax_rate_idis 0. second_tax_rate_idinteger- A second tax rate this ticket charges beside
tax_rate_id, or 0 for none. Each rate is charged on the price, never on the other's tax (Canada's GST and QST). reverse_chargestring- Who pays no VAT on this ticket:
""for nobody,countryfor companies inreverse_charge_country, oreufor companies in EU countries other than the account's own. A company qualifies only once the EU register confirms its VAT number while it books. reverse_charge_countrystring- The country whose companies get reverse charge, as an ISO 3166 code, when
reverse_chargeiscountry. Empty otherwise. reverse_charge_notestring- The organizer's own wording for the invoice of a reverse-charged order. Empty for the standard sentence.
colorstring- The ticket's color in the admin's lists: one of
slate,red,amber,green,teal,blue,indigoorpink, or empty for none. allow_downgradeboolean- Whether this type is ticked under the event's
allow_downgrades. A buyer may change a ticket of this type down to a cheaper one, and get the difference back, when both are on. The same name PATCH takes. allow_upgradeboolean- Whether this type is ticked under the event's
allow_upgrades. A buyer may change a ticket of this type up to a dearer one, and pay the difference, when both are on. The same name PATCH takes. holder_changes_modestringinheritthe event's rule, orallow/denyholder changes on this type.- one of
inheritallowdeny holder_changes_untilstring- The deadline for holder changes when the mode is allow, as the form stores it; empty for none, and always empty while the mode is inherit.
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- Tickets issued at this rung: seats on paid orders, on unpaid orders whose tickets already exist, and added to an order afterwards. 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.ticket_type_idsarray of integer- Every companion ticket type, any of which counts. They count together: one of each covers two.
companion.per_quantityinteger- How many of this type each set of companions covers.
0is once per order, however many of this type the order holds. companion.strictnessstringrecommendedshows people a tip and never blocks;requiredrefuses a checkout that falls short — unless no companion can be bought, in which case the rule pauses so this type stays sellable.- one of
recommendedrequired legacy_slugstring- The Tito Pro ticket type's slug, which a website moving from Tito Pro names in
data-tito-q-<slug>to preselect a quantity. Empty for a type made in Tito Go. Carried over by the account import. waitlistboolean- Whether the type opens a waitlist when it is fully booked. The people on it are at GET /events/{slug}/waitlist.
waitlist_head_startinteger- Minutes between one person on the waitlist being emailed about a free place and the next person. 60 unless changed. Only used while
waitlist_modeis first_come. - one of
1530601203601440 waitlist_modestring- What happens when a place becomes free. first_come emails everyone waiting, in the order they joined, a head start apart, and whoever takes the place first gets it. by_hand emails nobody: the free places show on the Waitlist screen and are offered by hand. first_come unless changed.
- one of
first_comeby_hand availableinteger or null- How many a buyer can still buy: the cap less what is sold and held, narrowed by the price ladder, less the places held for the waitlist. The figure the event page shows and an order created through this API is held to. null when the type is uncapped.
waitlist_heldinteger- How many free places are kept off sale for the people on the waitlist: the smaller of the number waiting (or offered a place) and the number free. 0 for a type with no waitlist, a bundle, or a type whose time is picked from a timetable.
sales_start_atstring (RFC 3339) or null- When the type goes on sale, RFC 3339; null for no start. Applies only while
on_saleis true: before it the type is listed andavailableis 0. PATCH takes it back as it is. sales_end_atstring (RFC 3339) or null- When the type comes off sale, RFC 3339; null for no end. The end is exclusive, and from it the type is listed with
available0.
Responses
201The new ticket type, in the same shape the list 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 or carried an unknown field, the name was blank, a price or capacity was negative or too large, the tax rate does not exist, orpricesnamed a currency the event does not sell in or disagreed withprice_cents(invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Early bird"}'
// 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: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Early bird"
}),
});
const data = await res.json();
{
"id": 1,
"name": "Early Bird",
"price_cents": 14900,
"currency": "eur",
"prices": {
"eur": 14900,
"gbp": 12900
},
"on_sale": true,
"quantity": 200,
"hide_free": false,
"signup_hide_name": false,
"pay_what_you_want": false,
"suggested_price_cents": 0,
"show_left_at": 0,
"sold": 142,
"description": "Includes lunch on both days.",
"description_text": "Includes lunch on both days.",
"description_html": "Includes \u003cstrong\u003elunch\u003c/strong\u003e on both days.",
"tax_rate_id": 0,
"price_includes_tax": false,
"second_tax_rate_id": 0,
"reverse_charge": "",
"reverse_charge_country": "",
"reverse_charge_note": "",
"color": "teal",
"max_per_order": 0,
"allow_downgrade": false,
"allow_upgrade": true,
"holder_changes_mode": "inherit",
"holder_changes_until": "",
"anonymous": false,
"qr_enabled": true,
"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,
"ticket_type_ids": [
2,
3
],
"quantity": 1,
"per_quantity": 1,
"strictness": "recommended"
},
"legacy_slug": "",
"waitlist": false,
"waitlist_head_start": 60,
"waitlist_mode": "first_come",
"available": 12,
"waitlist_held": 0,
"sales_start_at": "2027-01-01T12:00:00Z",
"sales_end_at": "2027-02-19T12:00:00Z"
}
Remove a ticket type
DELETE /events/{slug}/ticket-types/{id}
The ticket type's one remove control: a type nobody has bought is DELETED, one that has been used is ARCHIVED — taken out of the catalogue and off sale, its orders and tickets untouched — and the choice is made inside the same transaction, so a sale landing between your read and this call archives. The answer says which happened. An archived type can be restored; a deleted one cannot.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The ticket type's id, as
GET /events/{slug}/ticket-typeslists it.
Response fields
archivedboolean- The type had been used and is archived instead: out of the catalogue and off sale, orders and tickets untouched.
Responses
200What happened to it.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/ticket-types/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/ticket-types/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 7,
"deleted": false,
"archived": true
}
Get a ticket type
GET /events/{slug}/ticket-types/{id}
One ticket type of the event, in the list's shape. An archived type is not found, as it is absent from the list.
Requires catalog.view
Path parameters
slugstring required- The event's slug.
idinteger required- The ticket type's id, as
GET /events/{slug}/ticket-typeslists it.
Response fields
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.
hide_freeboolean- Whether the event page and the widget leave the word Free off this ticket. Only ever applies to a free ticket: a ticket with a price always shows it.
signup_hide_nameboolean- Whether a free event's sign-up form leaves this ticket's name and price off, showing only the questions and Confirm.
pay_what_you_wantboolean- Whether buyers choose this ticket's price. When true, price_cents and prices are the least a buyer can pay, and suggested_price_cents is what the price box on the event page and the widget starts on. Surfaces with no price box (invitations, waitlist offers, the API's own checkout) charge price_cents.
suggested_price_centsinteger- The price a pay-what-you-want ticket's box starts on, in minor units of the event's currency. 0 means no suggestion, and it is always 0 when pay_what_you_want is false.
show_left_atinteger- How few of this ticket have to be left before the event page and the widget say how many. 0 is never, and is the default.
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.
descriptionstring- Organizer text shown under the ticket's name. Empty when there is none. Plain text, or a composer body (the stored form of a message body) when it carries bold, italic, links or bullets. Send either back as it is. description_text and description_html are this read for a person.
description_textstring- The description as plain lines: a bullet starts with a bullet character and a link is its words followed by its address in brackets. The same as description when nothing is formatted.
description_htmlstring- The description as HTML, the way a buyer page draws it: escaped text whose line breaks are newlines, with strong, em, links that open in a new tab, and ul lists. Escaped plain text when nothing is formatted.
tax_rate_idinteger- The tax rate this ticket charges (
GET /tax-rates), or 0 for no tax. The same name the create and PATCH bodies take. price_includes_taxboolean- Whether
price_centsalready contains the tax. Always false whentax_rate_idis 0. second_tax_rate_idinteger- A second tax rate this ticket charges beside
tax_rate_id, or 0 for none. Each rate is charged on the price, never on the other's tax (Canada's GST and QST). reverse_chargestring- Who pays no VAT on this ticket:
""for nobody,countryfor companies inreverse_charge_country, oreufor companies in EU countries other than the account's own. A company qualifies only once the EU register confirms its VAT number while it books. reverse_charge_countrystring- The country whose companies get reverse charge, as an ISO 3166 code, when
reverse_chargeiscountry. Empty otherwise. reverse_charge_notestring- The organizer's own wording for the invoice of a reverse-charged order. Empty for the standard sentence.
colorstring- The ticket's color in the admin's lists: one of
slate,red,amber,green,teal,blue,indigoorpink, or empty for none. allow_downgradeboolean- Whether this type is ticked under the event's
allow_downgrades. A buyer may change a ticket of this type down to a cheaper one, and get the difference back, when both are on. The same name PATCH takes. allow_upgradeboolean- Whether this type is ticked under the event's
allow_upgrades. A buyer may change a ticket of this type up to a dearer one, and pay the difference, when both are on. The same name PATCH takes. holder_changes_modestringinheritthe event's rule, orallow/denyholder changes on this type.- one of
inheritallowdeny holder_changes_untilstring- The deadline for holder changes when the mode is allow, as the form stores it; empty for none, and always empty while the mode is inherit.
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- Tickets issued at this rung: seats on paid orders, on unpaid orders whose tickets already exist, and added to an order afterwards. 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.ticket_type_idsarray of integer- Every companion ticket type, any of which counts. They count together: one of each covers two.
companion.per_quantityinteger- How many of this type each set of companions covers.
0is once per order, however many of this type the order holds. companion.strictnessstringrecommendedshows people a tip and never blocks;requiredrefuses a checkout that falls short — unless no companion can be bought, in which case the rule pauses so this type stays sellable.- one of
recommendedrequired legacy_slugstring- The Tito Pro ticket type's slug, which a website moving from Tito Pro names in
data-tito-q-<slug>to preselect a quantity. Empty for a type made in Tito Go. Carried over by the account import. waitlistboolean- Whether the type opens a waitlist when it is fully booked. The people on it are at GET /events/{slug}/waitlist.
waitlist_head_startinteger- Minutes between one person on the waitlist being emailed about a free place and the next person. 60 unless changed. Only used while
waitlist_modeis first_come. - one of
1530601203601440 waitlist_modestring- What happens when a place becomes free. first_come emails everyone waiting, in the order they joined, a head start apart, and whoever takes the place first gets it. by_hand emails nobody: the free places show on the Waitlist screen and are offered by hand. first_come unless changed.
- one of
first_comeby_hand availableinteger or null- How many a buyer can still buy: the cap less what is sold and held, narrowed by the price ladder, less the places held for the waitlist. The figure the event page shows and an order created through this API is held to. null when the type is uncapped.
waitlist_heldinteger- How many free places are kept off sale for the people on the waitlist: the smaller of the number waiting (or offered a place) and the number free. 0 for a type with no waitlist, a bundle, or a type whose time is picked from a timetable.
sales_start_atstring (RFC 3339) or null- When the type goes on sale, RFC 3339; null for no start. Applies only while
on_saleis true: before it the type is listed andavailableis 0. PATCH takes it back as it is. sales_end_atstring (RFC 3339) or null- When the type comes off sale, RFC 3339; null for no end. The end is exclusive, and from it the type is listed with
available0.
Responses
200The ticket type.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/ticket-types/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/ticket-types/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 1,
"name": "Early Bird",
"price_cents": 14900,
"currency": "eur",
"prices": {
"eur": 14900,
"gbp": 12900
},
"on_sale": true,
"quantity": 200,
"hide_free": false,
"signup_hide_name": false,
"pay_what_you_want": false,
"suggested_price_cents": 0,
"show_left_at": 0,
"sold": 142,
"description": "Includes lunch on both days.",
"description_text": "Includes lunch on both days.",
"description_html": "Includes \u003cstrong\u003elunch\u003c/strong\u003e on both days.",
"tax_rate_id": 0,
"price_includes_tax": false,
"second_tax_rate_id": 0,
"reverse_charge": "",
"reverse_charge_country": "",
"reverse_charge_note": "",
"color": "teal",
"max_per_order": 0,
"allow_downgrade": false,
"allow_upgrade": true,
"holder_changes_mode": "inherit",
"holder_changes_until": "",
"anonymous": false,
"qr_enabled": true,
"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,
"ticket_type_ids": [
2,
3
],
"quantity": 1,
"per_quantity": 1,
"strictness": "recommended"
},
"legacy_slug": "",
"waitlist": false,
"waitlist_head_start": 60,
"waitlist_mode": "first_come",
"available": 12,
"waitlist_held": 0,
"sales_start_at": "2027-01-01T12:00:00Z",
"sales_end_at": "2027-02-19T12:00:00Z"
}
Update a ticket type
PATCH /events/{slug}/ticket-types/{id}
Both panels of the ticket type's form in one call. Absent fields are left unchanged; an explicit null is refused everywhere but quantity, where it means uncapped; an unknown field is refused. A Basics field and an Advanced field each record their own ticket_type.updated history row, exactly as the two tabs do. prices is merged by key: a currency present with an amount is set, present with null is cleared (the type is then not offered there), absent is unchanged. This writes what a SINGLE-PRICED ticket has: name, price, capacity, description, the tax decision, the Advanced switches, and the price in each extra currency. A type priced by a ladder, by volume bands or as a bundle takes its price and capacity from rows of its own, written at /events/{slug}/ticket-types/{id}/tiers, /events/{slug}/ticket-types/{id}/bands and /events/{slug}/ticket-types/{id}/bundle, so on one of those the price and capacity fields here are refused (pricing_mode) and everything else still saves. The companion and free-ticket rules have endpoints of their own, /events/{slug}/ticket-types/{id}/companion and /events/{slug}/ticket-types/{id}/free-with. The ticket's own time is admin-only. Ticket amounts are capped at 99,999,999 whole currency units plus the currency’s fractional minor units, matching the UI.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The ticket type's id, as
GET /events/{slug}/ticket-typeslists it.
Request body
price_centsinteger- The price in the event's currency. Refused on a laddered, banded or bundled type.
quantityinteger or null- Capacity; null is uncapped. Refused on a laddered, banded or bundled type.
descriptionstring- Shown under the name on the event page. Plain text, or a composer body carrying bold, italic, links (https or mailto) and bullets. Anything else in a body is kept as its words.
second_tax_rate_idinteger- A second of the account's tax rates, charged beside
tax_rate_idon the same price, or 0 for none. Needs atax_rate_id, and may not be the same rate; settingtax_rate_idto 0 takes it off too. reverse_chargestring""for nobody,country(withreverse_charge_country), oreu. Any of the three fields sent restates the rest from what is stored.pricesobject- Price per currency, lowercase code → cents. The event's own currency, if present, must equal
price_cents. On a PATCH,nullclears a currency (the type is then not offered there) and an absent key is unchanged. allow_downgradeboolean- Whether this type is ticked under the event's
allow_downgrades. A buyer may change a ticket of this type down to a cheaper one, and get the difference back, when both are on. allow_upgradeboolean- Whether this type is ticked under the event's
allow_upgrades. A buyer may change a ticket of this type up to a dearer one, and pay the difference, when both are on. holder_changes_modestringinheritthe event's rule, orallow/denyholder changes on this type.- one of
inheritallowdeny holder_changes_untilstring- The deadline for holder changes when the mode is allow, as the form stores it; cleared when the mode is inherit.
hide_freeboolean- Leave the word Free off this ticket on the event page and the widget. Only a free ticket with no price ladder can be set to true (422 otherwise): a price is never hidden.
signup_hide_nameboolean- Leave this ticket's name and price off a free event's sign-up form. Only a free ticket with no price ladder can be set to true (422 otherwise).
pay_what_you_wantboolean- Let buyers choose the price, with price_cents as the minimum. Refused with 409 pricing_mode on a ticket priced by tiers, volume prices or a bundle, and with 409 in_bundle on a ticket that is part of a bundle. While it is true, a PUT to the ticket type's tiers, bands or bundle answers 409 pricing_mode. false takes the suggestion off too.
suggested_price_centsinteger- The price the buyer's box starts on, 0 for none. Needs pay_what_you_want, and is never less than price_cents (422).
show_left_atinteger- 0 (never) or how few of this ticket have to be left before the event page and the widget say how many.
waitlistboolean- Open a waitlist when the type is fully booked. Switching it off keeps the people already on the list. A bundle cannot have a waitlist: switching one on for a bundle is refused. Neither, for now, can a type whose time is picked from a timetable on the event page: switching one on for it is refused too, and a list switched on before the timetable was added keeps taking names but holds no place and offers none.
waitlist_head_startinteger- Minutes between one person on the waitlist being emailed about a free place and the next person. Any other value is refused.
- one of
1530601203601440 waitlist_modestring- What happens when a place becomes free: first_come emails everyone waiting, a head start apart; by_hand emails nobody until a place is offered by hand. Any other value is refused.
- one of
first_comeby_hand sales_start_atstring or null- When the type goes on sale: on the event's clock (
2027-01-01T12:00), or RFC 3339 as the read answers; "" or null for no start. It applies only while the type is on sale. Sent alone, it is checked against the end as it stands. sales_end_atstring or null- When the type comes off sale: on the event's clock, or RFC 3339 as the read answers; "" or null for no end. It must come after
sales_start_at.
Response fields
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.
hide_freeboolean- Whether the event page and the widget leave the word Free off this ticket. Only ever applies to a free ticket: a ticket with a price always shows it.
signup_hide_nameboolean- Whether a free event's sign-up form leaves this ticket's name and price off, showing only the questions and Confirm.
pay_what_you_wantboolean- Whether buyers choose this ticket's price. When true, price_cents and prices are the least a buyer can pay, and suggested_price_cents is what the price box on the event page and the widget starts on. Surfaces with no price box (invitations, waitlist offers, the API's own checkout) charge price_cents.
suggested_price_centsinteger- The price a pay-what-you-want ticket's box starts on, in minor units of the event's currency. 0 means no suggestion, and it is always 0 when pay_what_you_want is false.
show_left_atinteger- How few of this ticket have to be left before the event page and the widget say how many. 0 is never, and is the default.
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.
descriptionstring- Organizer text shown under the ticket's name. Empty when there is none. Plain text, or a composer body (the stored form of a message body) when it carries bold, italic, links or bullets. Send either back as it is. description_text and description_html are this read for a person.
description_textstring- The description as plain lines: a bullet starts with a bullet character and a link is its words followed by its address in brackets. The same as description when nothing is formatted.
description_htmlstring- The description as HTML, the way a buyer page draws it: escaped text whose line breaks are newlines, with strong, em, links that open in a new tab, and ul lists. Escaped plain text when nothing is formatted.
tax_rate_idinteger- The tax rate this ticket charges (
GET /tax-rates), or 0 for no tax. The same name the create and PATCH bodies take. price_includes_taxboolean- Whether
price_centsalready contains the tax. Always false whentax_rate_idis 0. second_tax_rate_idinteger- A second tax rate this ticket charges beside
tax_rate_id, or 0 for none. Each rate is charged on the price, never on the other's tax (Canada's GST and QST). reverse_chargestring- Who pays no VAT on this ticket:
""for nobody,countryfor companies inreverse_charge_country, oreufor companies in EU countries other than the account's own. A company qualifies only once the EU register confirms its VAT number while it books. reverse_charge_countrystring- The country whose companies get reverse charge, as an ISO 3166 code, when
reverse_chargeiscountry. Empty otherwise. reverse_charge_notestring- The organizer's own wording for the invoice of a reverse-charged order. Empty for the standard sentence.
colorstring- The ticket's color in the admin's lists: one of
slate,red,amber,green,teal,blue,indigoorpink, or empty for none. allow_downgradeboolean- Whether this type is ticked under the event's
allow_downgrades. A buyer may change a ticket of this type down to a cheaper one, and get the difference back, when both are on. The same name PATCH takes. allow_upgradeboolean- Whether this type is ticked under the event's
allow_upgrades. A buyer may change a ticket of this type up to a dearer one, and pay the difference, when both are on. The same name PATCH takes. holder_changes_modestringinheritthe event's rule, orallow/denyholder changes on this type.- one of
inheritallowdeny holder_changes_untilstring- The deadline for holder changes when the mode is allow, as the form stores it; empty for none, and always empty while the mode is inherit.
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- Tickets issued at this rung: seats on paid orders, on unpaid orders whose tickets already exist, and added to an order afterwards. 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.ticket_type_idsarray of integer- Every companion ticket type, any of which counts. They count together: one of each covers two.
companion.per_quantityinteger- How many of this type each set of companions covers.
0is once per order, however many of this type the order holds. companion.strictnessstringrecommendedshows people a tip and never blocks;requiredrefuses a checkout that falls short — unless no companion can be bought, in which case the rule pauses so this type stays sellable.- one of
recommendedrequired legacy_slugstring- The Tito Pro ticket type's slug, which a website moving from Tito Pro names in
data-tito-q-<slug>to preselect a quantity. Empty for a type made in Tito Go. Carried over by the account import. waitlistboolean- Whether the type opens a waitlist when it is fully booked. The people on it are at GET /events/{slug}/waitlist.
waitlist_head_startinteger- Minutes between one person on the waitlist being emailed about a free place and the next person. 60 unless changed. Only used while
waitlist_modeis first_come. - one of
1530601203601440 waitlist_modestring- What happens when a place becomes free. first_come emails everyone waiting, in the order they joined, a head start apart, and whoever takes the place first gets it. by_hand emails nobody: the free places show on the Waitlist screen and are offered by hand. first_come unless changed.
- one of
first_comeby_hand availableinteger or null- How many a buyer can still buy: the cap less what is sold and held, narrowed by the price ladder, less the places held for the waitlist. The figure the event page shows and an order created through this API is held to. null when the type is uncapped.
waitlist_heldinteger- How many free places are kept off sale for the people on the waitlist: the smaller of the number waiting (or offered a place) and the number free. 0 for a type with no waitlist, a bundle, or a type whose time is picked from a timetable.
sales_start_atstring (RFC 3339) or null- When the type goes on sale, RFC 3339; null for no start. Applies only while
on_saleis true: before it the type is listed andavailableis 0. PATCH takes it back as it is. sales_end_atstring (RFC 3339) or null- When the type comes off sale, RFC 3339; null for no end. The end is exclusive, and from it the type is listed with
available0.
Responses
200The ticket type as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The type is priced by a ladder, by volume bands or as a bundle and the body touched its price or capacity (pricing_mode); or the new price would make a bundle this type is part of free (bundle_would_free) or unpriceable (bundle_now_tiered).422The body was unreadable, carried an unknown field or a refused null, blanked the name, or a value was out of bounds: a negative price, a capacity past the ceiling, a colour outside the palette, aholder_changes_modeother than inherit/allow/deny, amax_per_orderbelow 0, awaitlist_head_startthat is not one of the listed minutes, awaitlist_modeother than first_come/by_hand,waitlistswitched on for a bundle or for a type whose time is picked from a timetable, a tax rate that does not exist, or apriceskey the event does not sell in (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Early bird"}'
// 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/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Early bird"
}),
});
const data = await res.json();
{
"id": 1,
"name": "Early Bird",
"price_cents": 14900,
"currency": "eur",
"prices": {
"eur": 14900,
"gbp": 12900
},
"on_sale": true,
"quantity": 200,
"hide_free": false,
"signup_hide_name": false,
"pay_what_you_want": false,
"suggested_price_cents": 0,
"show_left_at": 0,
"sold": 142,
"description": "Includes lunch on both days.",
"description_text": "Includes lunch on both days.",
"description_html": "Includes \u003cstrong\u003elunch\u003c/strong\u003e on both days.",
"tax_rate_id": 0,
"price_includes_tax": false,
"second_tax_rate_id": 0,
"reverse_charge": "",
"reverse_charge_country": "",
"reverse_charge_note": "",
"color": "teal",
"max_per_order": 0,
"allow_downgrade": false,
"allow_upgrade": true,
"holder_changes_mode": "inherit",
"holder_changes_until": "",
"anonymous": false,
"qr_enabled": true,
"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,
"ticket_type_ids": [
2,
3
],
"quantity": 1,
"per_quantity": 1,
"strictness": "recommended"
},
"legacy_slug": "",
"waitlist": false,
"waitlist_head_start": 60,
"waitlist_mode": "first_come",
"available": 12,
"waitlist_held": 0,
"sales_start_at": "2027-01-01T12:00:00Z",
"sales_end_at": "2027-02-19T12:00:00Z"
}
Remove ticket volume prices
DELETE /events/{slug}/ticket-types/{id}/bands
Only active ticket types in the named event. Amounts are integer minor units. A band price applies to every ticket in the order once its quantity threshold is reached. Removes bands while preserving base price, capacity and extra prices. Referenced bands remain archived for history. Repeating DELETE is a no-op.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Response fields
price_centsintegerquantityintegerpricesobjectbandsarray of objectbands[].idintegerbands[].from_qtyintegerbands[].price_centsintegerbands[].pricesobjectwarningsarray of objectwarnings[].band_idintegerwarnings[].currencystring
Responses
200Current base price, capacity, bands, and unreachable-band warnings401No 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/ticket-types/1/bands \
-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/1/bands", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"price_cents": 14900,
"quantity": 2,
"prices": {},
"bands": [
{
"id": 1,
"from_qty": 1,
"price_cents": 14900,
"prices": {}
}
],
"warnings": [
{
"band_id": 1,
"currency": "eur",
"code": "unreachable"
}
]
}
Get ticket volume prices
GET /events/{slug}/ticket-types/{id}/bands
Only active ticket types in the named event. Amounts are integer minor units. A band price applies to every ticket in the order once its quantity threshold is reached.
Requires catalog.view
Path parameters
slugstring required- The event's slug.
idinteger required
Response fields
price_centsintegerquantityintegerpricesobjectbandsarray of objectbands[].idintegerbands[].from_qtyintegerbands[].price_centsintegerbands[].pricesobjectwarningsarray of objectwarnings[].band_idintegerwarnings[].currencystring
Responses
200Current base price, capacity, bands, and unreachable-band warnings401No 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/ticket-types/1/bands \
-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/1/bands", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"price_cents": 14900,
"quantity": 2,
"prices": {},
"bands": [
{
"id": 1,
"from_qty": 1,
"price_cents": 14900,
"prices": {}
}
],
"warnings": [
{
"band_id": 1,
"currency": "eur",
"code": "unreachable"
}
]
}
Replace ticket volume prices
PUT /events/{slug}/ticket-types/{id}/bands
Only active ticket types in the named event. Amounts are integer minor units. A band price applies to every ticket in the order once its quantity threshold is reached. Replaces all bands, base price, capacity and enabled extra-currency prices atomically. Existing positive IDs must belong to this ticket and be distinct. Zero creates a row. Thresholds increase and prices decrease. Each offered currency is checked so a larger order cannot cost less in total, except for unreachable bands above max_per_order, which are warnings. Omitted or null extra prices clear that currency. Referenced removed bands are archived. Containing bundles are repriced in the same transaction.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Request body
price_centsinteger requiredquantityinteger requiredpricesobjectbandsarray of object requiredbands[].idintegerbands[].from_qtyinteger requiredbands[].price_centsinteger requiredbands[].pricesobject
Response fields
price_centsintegerquantityintegerpricesobjectbandsarray of objectbands[].idintegerbands[].from_qtyintegerbands[].price_centsintegerbands[].pricesobjectwarningsarray of objectwarnings[].band_idintegerwarnings[].currencystring
Responses
200Current base price, capacity, bands, and unreachable-band warnings401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Tier or bundle pricing conflicts, or containing bundle discount would become invalid422Invalid band, price, capacity, currency, or ladder500Internal error.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types/1/bands \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"price_cents":14900,"quantity":2,"bands":[{"from_qty":1,"price_cents":14900}]}'
// 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/1/bands", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"price_cents": 14900,
"quantity": 2,
"bands": [
{
"from_qty": 1,
"price_cents": 14900
}
]
}),
});
const data = await res.json();
{
"price_cents": 14900,
"quantity": 2,
"prices": {},
"bands": [
{
"id": 1,
"from_qty": 1,
"price_cents": 14900,
"prices": {}
}
],
"warnings": [
{
"band_id": 1,
"currency": "eur",
"code": "unreachable"
}
]
}
Remove a ticket bundle
DELETE /events/{slug}/ticket-types/{id}/bundle
Remove all ticket and goods components and reset the discount. Keep the last derived price as an ordinary flat price.
Requires catalog.manage
Path parameters
slugstring requiredidinteger required
Response fields
bundleany
Responses
200Current bundle, or null for a flat ticket type.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such active ticket type in this event.500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types/1/bundle \
-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/1/bundle", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"bundle": null
}
Get a ticket bundle
GET /events/{slug}/ticket-types/{id}/bundle
What this ticket type bundles: the tickets and goods it includes, and its price. bundle is null for a ticket type that is not a bundle.
Requires catalog.view
Path parameters
slugstring requiredidinteger required
Response fields
bundleany
Responses
200Current bundle, or null for a flat ticket type.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such active ticket type in this event.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types/1/bundle \
-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/1/bundle", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"bundle": null
}
Replace a ticket bundle
PUT /events/{slug}/ticket-types/{id}/bundle
Replace composition and discount atomically. At most 20 rows total; distinct IDs within each family. Ticket IDs must be active in this event, not the owner, not bundles, and not priced by active tiers. Goods use active item product IDs. The owner cannot already be a constituent. Refuses types with active price tiers or quantity bands; remove those before configuring a bundle. Prices use integer currency minor units. Discount cannot exceed component prices. Other-currency prices include goods and require every part to have a price; discounts restrict the bundle to its base currency.
Requires catalog.manage
Path parameters
slugstring requiredidinteger required
Request body
ticketsarray of object requiredtickets[].idinteger requiredtickets[].quantityinteger requiredgoodsarray of objectgoods[].idinteger requiredgoods[].quantityinteger requireddiscount_centsinteger
Response fields
bundleany
Responses
200Current bundle, or null for a flat ticket type.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such active ticket type in this event.409The ticket type has active price tiers or volume bands.422Invalid composition, quantities or discount.500Internal error.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types/1/bundle \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"tickets":[{"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/ticket-types/1/bundle", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"tickets": [
{
"id": 1,
"quantity": 2
}
]
}),
});
const data = await res.json();
{
"bundle": null
}
Remove companion rule
DELETE /events/{slug}/ticket-types/{id}/companion
Clears the companion rule. The owner must be active. This operation requires events.manage and returns 204 when the rule is cleared.
Requires catalog.manage
Path parameters
slugstring requiredidinteger required
Responses
204Remove companion rule401No 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/ticket-types/1/companion \
-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/1/companion", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
Read companion rule
GET /events/{slug}/ticket-types/{id}/companion
Returns the companion rule for the ticket type. The response has a rule field with either null or:
json { "ticket_type_id": 123, "ticket_type_ids": [123, 124], "quantity": 1, "per_quantity": 3, "strictness": "required" }
This operation requires events.view. An archived owner returns 404.
Requires catalog.view
Path parameters
slugstring requiredidinteger required
Response fields
ruleany
Responses
200Read companion rule401No 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/ticket-types/1/companion \
-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/1/companion", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"rule": null
}
Set companion rule
PUT /events/{slug}/ticket-types/{id}/companion
Sets or replaces the companion rule. ticket_type_ids names every companion ticket type, any of which counts, and they count together; ticket_type_id alone names a single companion, and when both are sent it must be the first of ticket_type_ids. For each per_quantity of the owner ticket type, the rule requires or recommends quantity companions; a per_quantity of 0 asks for quantity once in any order holding the owner, however many of it, and per_quantity must be sent, 0 included. strictness must be recommended or required. quantity must be a positive integer.
Each companion must be another active product-backed ticket type in the same event. The owner must be active. This operation requires events.manage, returns the updated rule in a rule response envelope, and returns 422 for invalid data or 404 when the owner is missing. A failed replacement keeps the previous rule.
Requires catalog.manage
Path parameters
slugstring requiredidinteger required
Request body
ticket_type_idintegerticket_type_idsarray of integerquantityinteger requiredper_quantityinteger required
Response fields
ruleany
Responses
200Set companion rule401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid rule500Internal error.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types/1/companion \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"quantity":2,"per_quantity":2,"strictness":"recommended"}'
// 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/1/companion", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"quantity": 2,
"per_quantity": 2,
"strictness": "recommended"
}),
});
const data = await res.json();
{
"rule": null
}
Remove free-ticket rule
DELETE /events/{slug}/ticket-types/{id}/free-with
Clears the free-ticket rule. The owner must be active. This operation requires events.manage and returns 204 when the rule is cleared.
Requires catalog.manage
Path parameters
slugstring requiredidinteger required
Responses
204Remove free-ticket rule401No 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/ticket-types/1/free-with \
-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/1/free-with", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
Read free-ticket rule
GET /events/{slug}/ticket-types/{id}/free-with
Returns the free-ticket rule for the ticket type. The response has a rule field with either null or:
json { "ticket_type_id": 123, "qualifying_quantity": 3, "free_quantity": 1 }
This operation requires events.view. An archived owner returns 404. A rule whose qualifying ticket type is archived is omitted, matching the editor.
Requires catalog.view
Path parameters
slugstring requiredidinteger required
Response fields
ruleany
Responses
200Read free-ticket rule401No 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/ticket-types/1/free-with \
-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/1/free-with", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"rule": null
}
Set free-ticket rule
PUT /events/{slug}/ticket-types/{id}/free-with
Sets or replaces the free-ticket rule. All fields are required. The target ticket type qualifies for the offer. The owner ticket type receives free_quantity tickets for each qualifying_quantity of the target. Both quantities must be positive integers, and free_quantity cannot exceed 100.
The target must be another active product-backed ticket type in the same event. The owner must be active. This operation requires events.manage, returns the updated rule in a rule response envelope, and returns 422 for invalid data or 404 when the owner is missing. A failed replacement keeps the previous rule.
Requires catalog.manage
Path parameters
slugstring requiredidinteger required
Request body
ticket_type_idinteger requiredqualifying_quantityinteger requiredfree_quantityinteger required
Response fields
ruleany
Responses
200Set free-ticket rule401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid rule500Internal error.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types/1/free-with \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"ticket_type_id":1,"qualifying_quantity":2,"free_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/ticket-types/1/free-with", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"ticket_type_id": 1,
"qualifying_quantity": 2,
"free_quantity": 2
}),
});
const data = await res.json();
{
"rule": null
}
Take a ticket type off sale
POST /events/{slug}/ticket-types/{id}/off-sale
The show view's switch, the other way. Recorded only when the state actually changes. No request body.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The ticket type's id, as
GET /events/{slug}/ticket-typeslists it.
Response fields
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.
hide_freeboolean- Whether the event page and the widget leave the word Free off this ticket. Only ever applies to a free ticket: a ticket with a price always shows it.
signup_hide_nameboolean- Whether a free event's sign-up form leaves this ticket's name and price off, showing only the questions and Confirm.
pay_what_you_wantboolean- Whether buyers choose this ticket's price. When true, price_cents and prices are the least a buyer can pay, and suggested_price_cents is what the price box on the event page and the widget starts on. Surfaces with no price box (invitations, waitlist offers, the API's own checkout) charge price_cents.
suggested_price_centsinteger- The price a pay-what-you-want ticket's box starts on, in minor units of the event's currency. 0 means no suggestion, and it is always 0 when pay_what_you_want is false.
show_left_atinteger- How few of this ticket have to be left before the event page and the widget say how many. 0 is never, and is the default.
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.
descriptionstring- Organizer text shown under the ticket's name. Empty when there is none. Plain text, or a composer body (the stored form of a message body) when it carries bold, italic, links or bullets. Send either back as it is. description_text and description_html are this read for a person.
description_textstring- The description as plain lines: a bullet starts with a bullet character and a link is its words followed by its address in brackets. The same as description when nothing is formatted.
description_htmlstring- The description as HTML, the way a buyer page draws it: escaped text whose line breaks are newlines, with strong, em, links that open in a new tab, and ul lists. Escaped plain text when nothing is formatted.
tax_rate_idinteger- The tax rate this ticket charges (
GET /tax-rates), or 0 for no tax. The same name the create and PATCH bodies take. price_includes_taxboolean- Whether
price_centsalready contains the tax. Always false whentax_rate_idis 0. second_tax_rate_idinteger- A second tax rate this ticket charges beside
tax_rate_id, or 0 for none. Each rate is charged on the price, never on the other's tax (Canada's GST and QST). reverse_chargestring- Who pays no VAT on this ticket:
""for nobody,countryfor companies inreverse_charge_country, oreufor companies in EU countries other than the account's own. A company qualifies only once the EU register confirms its VAT number while it books. reverse_charge_countrystring- The country whose companies get reverse charge, as an ISO 3166 code, when
reverse_chargeiscountry. Empty otherwise. reverse_charge_notestring- The organizer's own wording for the invoice of a reverse-charged order. Empty for the standard sentence.
colorstring- The ticket's color in the admin's lists: one of
slate,red,amber,green,teal,blue,indigoorpink, or empty for none. allow_downgradeboolean- Whether this type is ticked under the event's
allow_downgrades. A buyer may change a ticket of this type down to a cheaper one, and get the difference back, when both are on. The same name PATCH takes. allow_upgradeboolean- Whether this type is ticked under the event's
allow_upgrades. A buyer may change a ticket of this type up to a dearer one, and pay the difference, when both are on. The same name PATCH takes. holder_changes_modestringinheritthe event's rule, orallow/denyholder changes on this type.- one of
inheritallowdeny holder_changes_untilstring- The deadline for holder changes when the mode is allow, as the form stores it; empty for none, and always empty while the mode is inherit.
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- Tickets issued at this rung: seats on paid orders, on unpaid orders whose tickets already exist, and added to an order afterwards. 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.ticket_type_idsarray of integer- Every companion ticket type, any of which counts. They count together: one of each covers two.
companion.per_quantityinteger- How many of this type each set of companions covers.
0is once per order, however many of this type the order holds. companion.strictnessstringrecommendedshows people a tip and never blocks;requiredrefuses a checkout that falls short — unless no companion can be bought, in which case the rule pauses so this type stays sellable.- one of
recommendedrequired legacy_slugstring- The Tito Pro ticket type's slug, which a website moving from Tito Pro names in
data-tito-q-<slug>to preselect a quantity. Empty for a type made in Tito Go. Carried over by the account import. waitlistboolean- Whether the type opens a waitlist when it is fully booked. The people on it are at GET /events/{slug}/waitlist.
waitlist_head_startinteger- Minutes between one person on the waitlist being emailed about a free place and the next person. 60 unless changed. Only used while
waitlist_modeis first_come. - one of
1530601203601440 waitlist_modestring- What happens when a place becomes free. first_come emails everyone waiting, in the order they joined, a head start apart, and whoever takes the place first gets it. by_hand emails nobody: the free places show on the Waitlist screen and are offered by hand. first_come unless changed.
- one of
first_comeby_hand availableinteger or null- How many a buyer can still buy: the cap less what is sold and held, narrowed by the price ladder, less the places held for the waitlist. The figure the event page shows and an order created through this API is held to. null when the type is uncapped.
waitlist_heldinteger- How many free places are kept off sale for the people on the waitlist: the smaller of the number waiting (or offered a place) and the number free. 0 for a type with no waitlist, a bundle, or a type whose time is picked from a timetable.
sales_start_atstring (RFC 3339) or null- When the type goes on sale, RFC 3339; null for no start. Applies only while
on_saleis true: before it the type is listed andavailableis 0. PATCH takes it back as it is. sales_end_atstring (RFC 3339) or null- When the type comes off sale, RFC 3339; null for no end. The end is exclusive, and from it the type is listed with
available0.
Responses
200The ticket type as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types/1/off-sale \
-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/1/off-sale", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 1,
"name": "Early Bird",
"price_cents": 14900,
"currency": "eur",
"prices": {
"eur": 14900,
"gbp": 12900
},
"on_sale": true,
"quantity": 200,
"hide_free": false,
"signup_hide_name": false,
"pay_what_you_want": false,
"suggested_price_cents": 0,
"show_left_at": 0,
"sold": 142,
"description": "Includes lunch on both days.",
"description_text": "Includes lunch on both days.",
"description_html": "Includes \u003cstrong\u003elunch\u003c/strong\u003e on both days.",
"tax_rate_id": 0,
"price_includes_tax": false,
"second_tax_rate_id": 0,
"reverse_charge": "",
"reverse_charge_country": "",
"reverse_charge_note": "",
"color": "teal",
"max_per_order": 0,
"allow_downgrade": false,
"allow_upgrade": true,
"holder_changes_mode": "inherit",
"holder_changes_until": "",
"anonymous": false,
"qr_enabled": true,
"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,
"ticket_type_ids": [
2,
3
],
"quantity": 1,
"per_quantity": 1,
"strictness": "recommended"
},
"legacy_slug": "",
"waitlist": false,
"waitlist_head_start": 60,
"waitlist_mode": "first_come",
"available": 12,
"waitlist_held": 0,
"sales_start_at": "2027-01-01T12:00:00Z",
"sales_end_at": "2027-02-19T12:00:00Z"
}
Put a ticket type on sale
POST /events/{slug}/ticket-types/{id}/on-sale
The show view's switch, one way. Recorded in the history only when the state actually changes; a type already on sale answers as it is. No request body.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The ticket type's id, as
GET /events/{slug}/ticket-typeslists it.
Response fields
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.
hide_freeboolean- Whether the event page and the widget leave the word Free off this ticket. Only ever applies to a free ticket: a ticket with a price always shows it.
signup_hide_nameboolean- Whether a free event's sign-up form leaves this ticket's name and price off, showing only the questions and Confirm.
pay_what_you_wantboolean- Whether buyers choose this ticket's price. When true, price_cents and prices are the least a buyer can pay, and suggested_price_cents is what the price box on the event page and the widget starts on. Surfaces with no price box (invitations, waitlist offers, the API's own checkout) charge price_cents.
suggested_price_centsinteger- The price a pay-what-you-want ticket's box starts on, in minor units of the event's currency. 0 means no suggestion, and it is always 0 when pay_what_you_want is false.
show_left_atinteger- How few of this ticket have to be left before the event page and the widget say how many. 0 is never, and is the default.
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.
descriptionstring- Organizer text shown under the ticket's name. Empty when there is none. Plain text, or a composer body (the stored form of a message body) when it carries bold, italic, links or bullets. Send either back as it is. description_text and description_html are this read for a person.
description_textstring- The description as plain lines: a bullet starts with a bullet character and a link is its words followed by its address in brackets. The same as description when nothing is formatted.
description_htmlstring- The description as HTML, the way a buyer page draws it: escaped text whose line breaks are newlines, with strong, em, links that open in a new tab, and ul lists. Escaped plain text when nothing is formatted.
tax_rate_idinteger- The tax rate this ticket charges (
GET /tax-rates), or 0 for no tax. The same name the create and PATCH bodies take. price_includes_taxboolean- Whether
price_centsalready contains the tax. Always false whentax_rate_idis 0. second_tax_rate_idinteger- A second tax rate this ticket charges beside
tax_rate_id, or 0 for none. Each rate is charged on the price, never on the other's tax (Canada's GST and QST). reverse_chargestring- Who pays no VAT on this ticket:
""for nobody,countryfor companies inreverse_charge_country, oreufor companies in EU countries other than the account's own. A company qualifies only once the EU register confirms its VAT number while it books. reverse_charge_countrystring- The country whose companies get reverse charge, as an ISO 3166 code, when
reverse_chargeiscountry. Empty otherwise. reverse_charge_notestring- The organizer's own wording for the invoice of a reverse-charged order. Empty for the standard sentence.
colorstring- The ticket's color in the admin's lists: one of
slate,red,amber,green,teal,blue,indigoorpink, or empty for none. allow_downgradeboolean- Whether this type is ticked under the event's
allow_downgrades. A buyer may change a ticket of this type down to a cheaper one, and get the difference back, when both are on. The same name PATCH takes. allow_upgradeboolean- Whether this type is ticked under the event's
allow_upgrades. A buyer may change a ticket of this type up to a dearer one, and pay the difference, when both are on. The same name PATCH takes. holder_changes_modestringinheritthe event's rule, orallow/denyholder changes on this type.- one of
inheritallowdeny holder_changes_untilstring- The deadline for holder changes when the mode is allow, as the form stores it; empty for none, and always empty while the mode is inherit.
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- Tickets issued at this rung: seats on paid orders, on unpaid orders whose tickets already exist, and added to an order afterwards. 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.ticket_type_idsarray of integer- Every companion ticket type, any of which counts. They count together: one of each covers two.
companion.per_quantityinteger- How many of this type each set of companions covers.
0is once per order, however many of this type the order holds. companion.strictnessstringrecommendedshows people a tip and never blocks;requiredrefuses a checkout that falls short — unless no companion can be bought, in which case the rule pauses so this type stays sellable.- one of
recommendedrequired legacy_slugstring- The Tito Pro ticket type's slug, which a website moving from Tito Pro names in
data-tito-q-<slug>to preselect a quantity. Empty for a type made in Tito Go. Carried over by the account import. waitlistboolean- Whether the type opens a waitlist when it is fully booked. The people on it are at GET /events/{slug}/waitlist.
waitlist_head_startinteger- Minutes between one person on the waitlist being emailed about a free place and the next person. 60 unless changed. Only used while
waitlist_modeis first_come. - one of
1530601203601440 waitlist_modestring- What happens when a place becomes free. first_come emails everyone waiting, in the order they joined, a head start apart, and whoever takes the place first gets it. by_hand emails nobody: the free places show on the Waitlist screen and are offered by hand. first_come unless changed.
- one of
first_comeby_hand availableinteger or null- How many a buyer can still buy: the cap less what is sold and held, narrowed by the price ladder, less the places held for the waitlist. The figure the event page shows and an order created through this API is held to. null when the type is uncapped.
waitlist_heldinteger- How many free places are kept off sale for the people on the waitlist: the smaller of the number waiting (or offered a place) and the number free. 0 for a type with no waitlist, a bundle, or a type whose time is picked from a timetable.
sales_start_atstring (RFC 3339) or null- When the type goes on sale, RFC 3339; null for no start. Applies only while
on_saleis true: before it the type is listed andavailableis 0. PATCH takes it back as it is. sales_end_atstring (RFC 3339) or null- When the type comes off sale, RFC 3339; null for no end. The end is exclusive, and from it the type is listed with
available0.
Responses
200The ticket type as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types/1/on-sale \
-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/1/on-sale", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 1,
"name": "Early Bird",
"price_cents": 14900,
"currency": "eur",
"prices": {
"eur": 14900,
"gbp": 12900
},
"on_sale": true,
"quantity": 200,
"hide_free": false,
"signup_hide_name": false,
"pay_what_you_want": false,
"suggested_price_cents": 0,
"show_left_at": 0,
"sold": 142,
"description": "Includes lunch on both days.",
"description_text": "Includes lunch on both days.",
"description_html": "Includes \u003cstrong\u003elunch\u003c/strong\u003e on both days.",
"tax_rate_id": 0,
"price_includes_tax": false,
"second_tax_rate_id": 0,
"reverse_charge": "",
"reverse_charge_country": "",
"reverse_charge_note": "",
"color": "teal",
"max_per_order": 0,
"allow_downgrade": false,
"allow_upgrade": true,
"holder_changes_mode": "inherit",
"holder_changes_until": "",
"anonymous": false,
"qr_enabled": true,
"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,
"ticket_type_ids": [
2,
3
],
"quantity": 1,
"per_quantity": 1,
"strictness": "recommended"
},
"legacy_slug": "",
"waitlist": false,
"waitlist_head_start": 60,
"waitlist_mode": "first_come",
"available": 12,
"waitlist_held": 0,
"sales_start_at": "2027-01-01T12:00:00Z",
"sales_end_at": "2027-02-19T12:00:00Z"
}
Restore an archived ticket type
POST /events/{slug}/ticket-types/{id}/restore
Back into the catalogue, OFF sale; the connections that sold it elsewhere (portals, audiences, invitations) do not come back. 409 not_archived on a type that is not archived. No request body.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The ticket type's id, as
GET /events/{slug}/ticket-typeslists it.
Response fields
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.
hide_freeboolean- Whether the event page and the widget leave the word Free off this ticket. Only ever applies to a free ticket: a ticket with a price always shows it.
signup_hide_nameboolean- Whether a free event's sign-up form leaves this ticket's name and price off, showing only the questions and Confirm.
pay_what_you_wantboolean- Whether buyers choose this ticket's price. When true, price_cents and prices are the least a buyer can pay, and suggested_price_cents is what the price box on the event page and the widget starts on. Surfaces with no price box (invitations, waitlist offers, the API's own checkout) charge price_cents.
suggested_price_centsinteger- The price a pay-what-you-want ticket's box starts on, in minor units of the event's currency. 0 means no suggestion, and it is always 0 when pay_what_you_want is false.
show_left_atinteger- How few of this ticket have to be left before the event page and the widget say how many. 0 is never, and is the default.
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.
descriptionstring- Organizer text shown under the ticket's name. Empty when there is none. Plain text, or a composer body (the stored form of a message body) when it carries bold, italic, links or bullets. Send either back as it is. description_text and description_html are this read for a person.
description_textstring- The description as plain lines: a bullet starts with a bullet character and a link is its words followed by its address in brackets. The same as description when nothing is formatted.
description_htmlstring- The description as HTML, the way a buyer page draws it: escaped text whose line breaks are newlines, with strong, em, links that open in a new tab, and ul lists. Escaped plain text when nothing is formatted.
tax_rate_idinteger- The tax rate this ticket charges (
GET /tax-rates), or 0 for no tax. The same name the create and PATCH bodies take. price_includes_taxboolean- Whether
price_centsalready contains the tax. Always false whentax_rate_idis 0. second_tax_rate_idinteger- A second tax rate this ticket charges beside
tax_rate_id, or 0 for none. Each rate is charged on the price, never on the other's tax (Canada's GST and QST). reverse_chargestring- Who pays no VAT on this ticket:
""for nobody,countryfor companies inreverse_charge_country, oreufor companies in EU countries other than the account's own. A company qualifies only once the EU register confirms its VAT number while it books. reverse_charge_countrystring- The country whose companies get reverse charge, as an ISO 3166 code, when
reverse_chargeiscountry. Empty otherwise. reverse_charge_notestring- The organizer's own wording for the invoice of a reverse-charged order. Empty for the standard sentence.
colorstring- The ticket's color in the admin's lists: one of
slate,red,amber,green,teal,blue,indigoorpink, or empty for none. allow_downgradeboolean- Whether this type is ticked under the event's
allow_downgrades. A buyer may change a ticket of this type down to a cheaper one, and get the difference back, when both are on. The same name PATCH takes. allow_upgradeboolean- Whether this type is ticked under the event's
allow_upgrades. A buyer may change a ticket of this type up to a dearer one, and pay the difference, when both are on. The same name PATCH takes. holder_changes_modestringinheritthe event's rule, orallow/denyholder changes on this type.- one of
inheritallowdeny holder_changes_untilstring- The deadline for holder changes when the mode is allow, as the form stores it; empty for none, and always empty while the mode is inherit.
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- Tickets issued at this rung: seats on paid orders, on unpaid orders whose tickets already exist, and added to an order afterwards. 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.ticket_type_idsarray of integer- Every companion ticket type, any of which counts. They count together: one of each covers two.
companion.per_quantityinteger- How many of this type each set of companions covers.
0is once per order, however many of this type the order holds. companion.strictnessstringrecommendedshows people a tip and never blocks;requiredrefuses a checkout that falls short — unless no companion can be bought, in which case the rule pauses so this type stays sellable.- one of
recommendedrequired legacy_slugstring- The Tito Pro ticket type's slug, which a website moving from Tito Pro names in
data-tito-q-<slug>to preselect a quantity. Empty for a type made in Tito Go. Carried over by the account import. waitlistboolean- Whether the type opens a waitlist when it is fully booked. The people on it are at GET /events/{slug}/waitlist.
waitlist_head_startinteger- Minutes between one person on the waitlist being emailed about a free place and the next person. 60 unless changed. Only used while
waitlist_modeis first_come. - one of
1530601203601440 waitlist_modestring- What happens when a place becomes free. first_come emails everyone waiting, in the order they joined, a head start apart, and whoever takes the place first gets it. by_hand emails nobody: the free places show on the Waitlist screen and are offered by hand. first_come unless changed.
- one of
first_comeby_hand availableinteger or null- How many a buyer can still buy: the cap less what is sold and held, narrowed by the price ladder, less the places held for the waitlist. The figure the event page shows and an order created through this API is held to. null when the type is uncapped.
waitlist_heldinteger- How many free places are kept off sale for the people on the waitlist: the smaller of the number waiting (or offered a place) and the number free. 0 for a type with no waitlist, a bundle, or a type whose time is picked from a timetable.
sales_start_atstring (RFC 3339) or null- When the type goes on sale, RFC 3339; null for no start. Applies only while
on_saleis true: before it the type is listed andavailableis 0. PATCH takes it back as it is. sales_end_atstring (RFC 3339) or null- When the type comes off sale, RFC 3339; null for no end. The end is exclusive, and from it the type is listed with
available0.
Responses
200The ticket type as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The type is not archived (not_archived).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types/1/restore \
-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/1/restore", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 1,
"name": "Early Bird",
"price_cents": 14900,
"currency": "eur",
"prices": {
"eur": 14900,
"gbp": 12900
},
"on_sale": true,
"quantity": 200,
"hide_free": false,
"signup_hide_name": false,
"pay_what_you_want": false,
"suggested_price_cents": 0,
"show_left_at": 0,
"sold": 142,
"description": "Includes lunch on both days.",
"description_text": "Includes lunch on both days.",
"description_html": "Includes \u003cstrong\u003elunch\u003c/strong\u003e on both days.",
"tax_rate_id": 0,
"price_includes_tax": false,
"second_tax_rate_id": 0,
"reverse_charge": "",
"reverse_charge_country": "",
"reverse_charge_note": "",
"color": "teal",
"max_per_order": 0,
"allow_downgrade": false,
"allow_upgrade": true,
"holder_changes_mode": "inherit",
"holder_changes_until": "",
"anonymous": false,
"qr_enabled": true,
"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,
"ticket_type_ids": [
2,
3
],
"quantity": 1,
"per_quantity": 1,
"strictness": "recommended"
},
"legacy_slug": "",
"waitlist": false,
"waitlist_head_start": 60,
"waitlist_mode": "first_come",
"available": 12,
"waitlist_held": 0,
"sales_start_at": "2027-01-01T12:00:00Z",
"sales_end_at": "2027-02-19T12:00:00Z"
}
Replace ticket tiers with one flat price
DELETE /events/{slug}/ticket-types/{id}/tiers
Active tiers only. Price amounts use integer currency minor units. Starts and ends are Unix seconds, with zero meaning no bound. Quantity -1 is unlimited and zero offers no places. Requires replacement flat price_cents and quantity in the JSON body. Replaces enabled extra-currency prices as well, clearing omitted ones. Clears tiers and fallback settings atomically, retaining referenced tiers as history. A type with no active tiers returns 409.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Request body
Response fields
tiersarray of objecttiers[].idintegertiers[].namestringtiers[].price_centsintegertiers[].quantityintegertiers[].starts_atintegertiers[].ends_atintegertiers[].pricesobjectcascadebooleanfallbackbooleanfallback_price_centsintegerfallback_pricesobject
Responses
200Current active tiers and settings401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Conflicting pricing mode422Invalid tier, price, capacity, time window or currency500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types/1/tiers \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"price_cents":14900,"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/ticket-types/1/tiers", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"price_cents": 14900,
"quantity": 2
}),
});
const data = await res.json();
{
"tiers": [
{
"id": 1,
"name": "Ada Lovelace",
"price_cents": 14900,
"quantity": 2,
"starts_at": 1,
"ends_at": 1,
"prices": {}
}
],
"cascade": true,
"fallback": true,
"fallback_price_cents": 14900,
"fallback_prices": {}
}
Get ticket price tiers
GET /events/{slug}/ticket-types/{id}/tiers
Active tiers only. Price amounts use integer currency minor units. Starts and ends are Unix seconds, with zero meaning no bound. Quantity -1 is unlimited and zero offers no places.
Requires catalog.view
Path parameters
slugstring required- The event's slug.
idinteger required
Response fields
tiersarray of objecttiers[].idintegertiers[].namestringtiers[].price_centsintegertiers[].quantityintegertiers[].starts_atintegertiers[].ends_atintegertiers[].pricesobjectcascadebooleanfallbackbooleanfallback_price_centsintegerfallback_pricesobject
Responses
200Current active tiers and settings401No 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/ticket-types/1/tiers \
-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/1/tiers", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"tiers": [
{
"id": 1,
"name": "Ada Lovelace",
"price_cents": 14900,
"quantity": 2,
"starts_at": 1,
"ends_at": 1,
"prices": {}
}
],
"cascade": true,
"fallback": true,
"fallback_price_cents": 14900,
"fallback_prices": {}
}
Replace ticket price tiers
PUT /events/{slug}/ticket-types/{id}/tiers
Active tiers only. Price amounts use integer currency minor units. Starts and ends are Unix seconds, with zero meaning no bound. Quantity -1 is unlimited and zero offers no places. Replaces all tiers and settings atomically. Existing positive IDs must be distinct and belong to this active ladder. Zero creates a new row. Omitted referenced rows are archived, others deleted. Omitted extra-currency prices are cleared. Bundle owners, bundle members, and types with volume pricing return 409. Capacity becomes the tier total plus archived tier seats.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Request body
tiersarray of object requiredtiers[].idintegertiers[].namestringtiers[].price_centsinteger requiredtiers[].quantityinteger requiredtiers[].starts_atintegertiers[].ends_atintegertiers[].pricesobjectcascadebooleanfallbackbooleanfallback_price_centsintegerfallback_pricesobject
Response fields
tiersarray of objecttiers[].idintegertiers[].namestringtiers[].price_centsintegertiers[].quantityintegertiers[].starts_atintegertiers[].ends_atintegertiers[].pricesobjectcascadebooleanfallbackbooleanfallback_price_centsintegerfallback_pricesobject
Responses
200Current active tiers and settings401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Conflicting pricing mode422Invalid tier, price, capacity, time window or currency500Internal error.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/events/annual-conf/ticket-types/1/tiers \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"tiers":[{"price_cents":14900,"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/ticket-types/1/tiers", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"tiers": [
{
"price_cents": 14900,
"quantity": 2
}
]
}),
});
const data = await res.json();
{
"tiers": [
{
"id": 1,
"name": "Ada Lovelace",
"price_cents": 14900,
"quantity": 2,
"starts_at": 1,
"ends_at": 1,
"prices": {}
}
],
"cascade": true,
"fallback": true,
"fallback_price_cents": 14900,
"fallback_prices": {}
}
Questions
List where fields are asked on an event
GET /events/{slug}/fields
Every place a field is asked or recorded on this event: the order form, each ticket type, good, donation, portal, application page, session and goods group, with the target named.
Requires catalog.view
Path parameters
slugstring required- The event's slug.
Response fields
attachmentsarray of FieldAttachmentattachments[].condition_field_idintegerattachments[].condition_valuestringattachments[].condition_rulesarray of objectattachments[].condition_rules[].parent_idintegerattachments[].condition_rules[].valuesarray of stringattachments[].target_typestring- Where it is asked.
orderis the order form, target_id 0. - one of
orderticket_typeitemdonationportalapplicationsessiongoods_groupwaitlist attachments[].positioninteger- The field's place among the target's questions; 0 when never arranged by hand.
attachments[].label_overridestring- A label used on this target instead of the field's own; empty for none.
attachments[].excludedboolean- A portal row that is an exception to "asked on every portal" rather than an attachment.
Responses
200Attachments, by target then position.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/fields \
-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/fields", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"attachments": [
{
"condition_field_id": 1,
"condition_value": "example",
"condition_rules": [
{
"parent_id": 1,
"operator": "equals",
"values": [
"example"
]
}
],
"id": 3,
"field_id": 12,
"field_label": "T-shirt size",
"target_type": "ticket_type",
"target_id": 7,
"target_name": "VIP",
"required": true,
"position": 0,
"label_override": "",
"excluded": false
}
]
}
Ask a field somewhere on an event
POST /events/{slug}/fields
Attach a library field to a target of this event. target_type is order (the order form, target_id 0), ticket_type, item, donation, portal, application, session, goods_group or waitlist (target_id a ticket type's id), and target_id must be that kind of row ON THIS EVENT. required means anything only of a question somebody is asked, so it is stored false on a readonly or secret row; such rows can only be attached to a ticket type. A file field goes only on the order form or a portal. Attaching where the field already is updates required and answers 200.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
Request body
target_typestring required- Where to ask it.
- one of
orderticket_typeitemdonationportalapplicationsessiongoods_groupwaitlist
Response fields
condition_field_idintegercondition_valuestringcondition_rulesarray of objectcondition_rules[].parent_idintegercondition_rules[].valuesarray of stringtarget_typestring- Where it is asked.
orderis the order form, target_id 0. - one of
orderticket_typeitemdonationportalapplicationsessiongoods_groupwaitlist excludedboolean- A portal row that is an exception to "asked on every portal" rather than an attachment.
Response fields
condition_field_idintegercondition_valuestringcondition_rulesarray of objectcondition_rules[].parent_idintegercondition_rules[].valuesarray of stringtarget_typestring- Where it is asked.
orderis the order form, target_id 0. - one of
orderticket_typeitemdonationportalapplicationsessiongoods_groupwaitlist excludedboolean- A portal row that is an exception to "asked on every portal" rather than an attachment.
Responses
201The attachment.200The attachment, already there, withrequiredupdated.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body was unreadable or carried an unknown field, the field does not exist or is archived or event-scoped, the target kind is unknown or the id is not this event's, or the field's mode or kind cannot go on that target (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/fields \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"field_id":12,"target_type":"ticket_type","target_id":7}'
// 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/fields", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"field_id": 12,
"target_type": "ticket_type",
"target_id": 7
}),
});
const data = await res.json();
The attachment.
{
"condition_field_id": 1,
"condition_value": "example",
"condition_rules": [
{
"parent_id": 1,
"operator": "equals",
"values": [
"example"
]
}
],
"id": 3,
"field_id": 12,
"field_label": "T-shirt size",
"target_type": "ticket_type",
"target_id": 7,
"target_name": "VIP",
"required": true,
"position": 0,
"label_override": "",
"excluded": false
}
The attachment, already there, with required updated.
{
"condition_field_id": 1,
"condition_value": "example",
"condition_rules": [
{
"parent_id": 1,
"operator": "equals",
"values": [
"example"
]
}
],
"id": 3,
"field_id": 12,
"field_label": "T-shirt size",
"target_type": "ticket_type",
"target_id": 7,
"target_name": "VIP",
"required": true,
"position": 0,
"label_override": "",
"excluded": false
}
Order event questions
POST /events/{slug}/fields/order
Named fields come first. Unnamed fields keep their current relative order after them. An empty field_ids array keeps the current order. Missing or null arrays, duplicate IDs, and IDs outside the selected active library or target return 422. A failed database write leaves the previous order intact.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
Request body
field_idsarray of integer requiredtarget_typestring required- Where to ask it.
- one of
orderticket_typeitemdonationportalapplicationsessiongoods_groupwaitlist
Response fields
attachmentsarray of FieldAttachmentattachments[].condition_field_idintegerattachments[].condition_valuestringattachments[].condition_rulesarray of objectattachments[].condition_rules[].parent_idintegerattachments[].condition_rules[].valuesarray of stringattachments[].target_typestring- Where it is asked.
orderis the order form, target_id 0. - one of
orderticket_typeitemdonationportalapplicationsessiongoods_groupwaitlist attachments[].positioninteger- The field's place among the target's questions; 0 when never arranged by hand.
attachments[].label_overridestring- A label used on this target instead of the field's own; empty for none.
attachments[].excludedboolean- A portal row that is an exception to "asked on every portal" rather than an attachment.
Responses
200Attachments, by target then position.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid field order or target.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/fields/order \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"field_ids":[1],"target_type":"ticket_type","target_id":7}'
// 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/fields/order", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"field_ids": [
1
],
"target_type": "ticket_type",
"target_id": 7
}),
});
const data = await res.json();
{
"attachments": [
{
"condition_field_id": 1,
"condition_value": "example",
"condition_rules": [
{
"parent_id": 1,
"operator": "equals",
"values": [
"example"
]
}
],
"id": 3,
"field_id": 12,
"field_label": "T-shirt size",
"target_type": "ticket_type",
"target_id": 7,
"target_name": "VIP",
"required": true,
"position": 0,
"label_override": "",
"excluded": false
}
]
}
Stop asking a field somewhere on an event
DELETE /events/{slug}/fields/{id}
Remove one attachment. The answers already given keep their rows; only the asking stops.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The attachment's id, as
GET /events/{slug}/fieldslists it.
Response fields
Responses
200Detached.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/fields/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/fields/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"detached": true
}
List fields
GET /fields
The fields library is every question asked of people and every piece of metadata recorded about them, account-wide; mode is what tells them apart (the admin shows writable rows under Questions and the rest under Settings → Metadata). Details about the EVENT are a different surface: GET /events/{slug} reports their values. Live fields in the account's own library order; ?archived=1 lists the archived ones instead.
Requires catalog.view
Query parameters
archivedstring1lists archived fields instead of live ones.
Response fields
fieldsarray of Fieldfields[].kindstring- What kind of answer it takes.
- one of
texttextareaselectmulti_selectcheckboxyes_nodatenumberphonecountryfile fields[].optionsarray of string- The choices of a select or multi_select, in order, as stored. Empty on every other kind.
fields[].scopestring- Always
personhere; event-scope details are reported on the event. - one of
person fields[].modestringwritable: attendees answer it and see it (a question).readonly: organizers record it, attendees see it.secret: organizers record it, only they see it.- one of
writablereadonlysecret fields[].max_filesinteger- How many files one person may upload on a file field (1–10); 1 on every other kind.
fields[].other_choiceboolean- A select or multi_select ends in a write-in answer: what the person writes is stored as the answer itself. false on every other kind.
fields[].other_labelstring- The organizer's own word for the write-in row; empty means the standard “Other” in the buyer's language.
fields[].country_onlyarray of string- country fields only: the countries offered, as ISO 3166-1 alpha-2 codes; empty offers every country. A country field's answers are these codes too. Empty on every other kind.
fields[].country_pinnedarray of string- country fields only: the countries that head the list, in this order, as ISO 3166-1 alpha-2 codes. A pinned country is always offered. Empty on every other kind.
Responses
200Fields, in library order.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/fields \
-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/fields", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"fields": [
{
"id": 12,
"key": "t-shirt-size",
"label": "T-shirt size",
"kind": "select",
"options": [
"S",
"M",
"L"
],
"scope": "person",
"mode": "writable",
"description": "For the goody bag.",
"max_files": 1,
"other_choice": false,
"other_label": "",
"country_only": [],
"country_pinned": [
"AE",
"SA",
"GB",
"IN"
],
"position": 3,
"archived": false
}
]
}
Create a field
POST /fields
A new person-scope field in the library. A blank key derives from the label and takes a numeric suffix past a collision; keys are unique and never change, because answers reference them. mode defaults to writable, which is what makes a row a question. A select or multi_select needs at least one option. A file field is refused when the instance has no object storage, and can only ever be asked on the order form or a portal.
Requires catalog.manage
Request body
kindstring required- What kind of answer it takes.
- one of
texttextareaselectmulti_selectcheckboxyes_nodatenumberphonecountryfile optionsarray of string- The choices of a select or multi_select, in order, as stored. Empty on every other kind.
country_onlyarray of string- country fields only: the countries offered, as ISO 3166-1 alpha-2 codes; empty offers every country. A country field's answers are these codes too.
country_pinnedarray of string- country fields only: the countries that head the list, in this order, as ISO 3166-1 alpha-2 codes. A pinned country is always offered.
Response fields
kindstring- What kind of answer it takes.
- one of
texttextareaselectmulti_selectcheckboxyes_nodatenumberphonecountryfile optionsarray of string- The choices of a select or multi_select, in order, as stored. Empty on every other kind.
modestringwritable: attendees answer it and see it (a question).readonly: organizers record it, attendees see it.secret: organizers record it, only they see it.- one of
writablereadonlysecret max_filesinteger- How many files one person may upload on a file field (1–10); 1 on every other kind.
other_choiceboolean- A select or multi_select ends in a write-in answer: what the person writes is stored as the answer itself. false on every other kind.
other_labelstring- The organizer's own word for the write-in row; empty means the standard “Other” in the buyer's language.
country_onlyarray of string- country fields only: the countries offered, as ISO 3166-1 alpha-2 codes; empty offers every country. A country field's answers are these codes too. Empty on every other kind.
country_pinnedarray of string- country fields only: the countries that head the list, in this order, as ISO 3166-1 alpha-2 codes. A pinned country is always offered. Empty on every other kind.
Responses
201The new field.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422The body was unreadable or carried an unknown field, the label was blank or unusable, the kind or mode is unknown, a choice field has no options, or file fields are unavailable (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/fields \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"label":"T-shirt size","kind":"select"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/fields", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"label": "T-shirt size",
"kind": "select"
}),
});
const data = await res.json();
{
"id": 12,
"key": "t-shirt-size",
"label": "T-shirt size",
"kind": "select",
"options": [
"S",
"M",
"L"
],
"scope": "person",
"mode": "writable",
"description": "For the goody bag.",
"max_files": 1,
"other_choice": false,
"other_label": "",
"country_only": [],
"country_pinned": [
"AE",
"SA",
"GB",
"IN"
],
"position": 3,
"archived": false
}
Order fields
POST /fields/order
Named fields come first. Unnamed fields keep their current relative order after them. An empty field_ids array keeps the current order. Missing or null arrays, duplicate IDs, and IDs outside the selected active library or target return 422. A failed database write leaves the previous order intact.
Requires catalog.manage
Request body
field_idsarray of integer required
Response fields
fieldsarray of Fieldfields[].kindstring- What kind of answer it takes.
- one of
texttextareaselectmulti_selectcheckboxyes_nodatenumberphonecountryfile fields[].optionsarray of string- The choices of a select or multi_select, in order, as stored. Empty on every other kind.
fields[].scopestring- Always
personhere; event-scope details are reported on the event. - one of
person fields[].modestringwritable: attendees answer it and see it (a question).readonly: organizers record it, attendees see it.secret: organizers record it, only they see it.- one of
writablereadonlysecret fields[].max_filesinteger- How many files one person may upload on a file field (1–10); 1 on every other kind.
fields[].other_choiceboolean- A select or multi_select ends in a write-in answer: what the person writes is stored as the answer itself. false on every other kind.
fields[].other_labelstring- The organizer's own word for the write-in row; empty means the standard “Other” in the buyer's language.
fields[].country_onlyarray of string- country fields only: the countries offered, as ISO 3166-1 alpha-2 codes; empty offers every country. A country field's answers are these codes too. Empty on every other kind.
fields[].country_pinnedarray of string- country fields only: the countries that head the list, in this order, as ISO 3166-1 alpha-2 codes. A pinned country is always offered. Empty on every other kind.
Responses
200Fields, in library order.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.422Invalid field order or target.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/fields/order \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"field_ids":[1]}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/fields/order", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"field_ids": [
1
]
}),
});
const data = await res.json();
{
"fields": [
{
"id": 12,
"key": "t-shirt-size",
"label": "T-shirt size",
"kind": "select",
"options": [
"S",
"M",
"L"
],
"scope": "person",
"mode": "writable",
"description": "For the goody bag.",
"max_files": 1,
"other_choice": false,
"other_label": "",
"country_only": [],
"country_pinned": [
"AE",
"SA",
"GB",
"IN"
],
"position": 3,
"archived": false
}
]
}
Remove a field
DELETE /fields/{id}
The one remove control: a field nobody has answered anywhere is DELETED with its attachments; one with an answer is ARCHIVED so the answers keep their meaning, and the choice is made inside the same transaction. The answer says which.
Requires catalog.manage
Path parameters
idinteger required- The field's id, as
GET /fieldslists it.
Response fields
Responses
200What happened to it.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The field is already archived (archived).500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/fields/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/fields/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 12,
"deleted": true,
"archived": false
}
Get a field
GET /fields/{id}
One field of the library, live or archived. An event-scope detail is not found here.
Requires catalog.view
Path parameters
idinteger required- The field's id, as
GET /fieldslists it.
Response fields
kindstring- What kind of answer it takes.
- one of
texttextareaselectmulti_selectcheckboxyes_nodatenumberphonecountryfile optionsarray of string- The choices of a select or multi_select, in order, as stored. Empty on every other kind.
modestringwritable: attendees answer it and see it (a question).readonly: organizers record it, attendees see it.secret: organizers record it, only they see it.- one of
writablereadonlysecret max_filesinteger- How many files one person may upload on a file field (1–10); 1 on every other kind.
other_choiceboolean- A select or multi_select ends in a write-in answer: what the person writes is stored as the answer itself. false on every other kind.
other_labelstring- The organizer's own word for the write-in row; empty means the standard “Other” in the buyer's language.
country_onlyarray of string- country fields only: the countries offered, as ISO 3166-1 alpha-2 codes; empty offers every country. A country field's answers are these codes too. Empty on every other kind.
country_pinnedarray of string- country fields only: the countries that head the list, in this order, as ISO 3166-1 alpha-2 codes. A pinned country is always offered. Empty on every other kind.
Responses
200The field.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/fields/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/fields/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 12,
"key": "t-shirt-size",
"label": "T-shirt size",
"kind": "select",
"options": [
"S",
"M",
"L"
],
"scope": "person",
"mode": "writable",
"description": "For the goody bag.",
"max_files": 1,
"other_choice": false,
"other_label": "",
"country_only": [],
"country_pinned": [
"AE",
"SA",
"GB",
"IN"
],
"position": 3,
"archived": false
}
Update a field
PATCH /fields/{id}
Absent fields are left unchanged; an explicit null and an unknown field are refused. options replaces the whole list when sent. Key and scope are fixed at creation and are not here; kind may change (never to or from file; sending the kind the field already has changes nothing) — see the kind field below and the 409/422 responses. A file field's mode is always writable, as the form keeps it. An archived field is 409 archived; restore it first.
Requires catalog.manage
Path parameters
idinteger required- The field's id, as
GET /fieldslists it.
Request body
kindstring- Changes the field's type (never to or from file; the kind the field already has is accepted and changes nothing). Runs in its own step, after the checks that need only this request (label, mode, max_files, the country lists, other_choice and other_label) and before the rest of the save: every answer already given is checked against the new kind, and a select or multi_select target takes
optionsfrom this same request if given, else the field's own stored choices, cleaned the same way (trimmed, blanks dropped, exact duplicates dropped keeping first-seen order). - one of
texttextareayes_noselectmulti_selectcheckboxdatenumberphonecountry optionsarray of string- The choices of a select or multi_select, in order, as stored. Empty on every other kind.
other_labelstring- select and multi_select only: the organizer's own word for the write-in row; empty for the standard “Other”. Cleared when other_choice is false.
country_onlyarray of string- country fields only: the countries offered, as ISO 3166-1 alpha-2 codes; empty offers every country. A country field's answers are these codes too. Replaces the whole list when sent.
country_pinnedarray of string- country fields only: the countries that head the list, in this order, as ISO 3166-1 alpha-2 codes. A pinned country is always offered. Replaces the whole list when sent.
Response fields
kindstring- What kind of answer it takes.
- one of
texttextareaselectmulti_selectcheckboxyes_nodatenumberphonecountryfile optionsarray of string- The choices of a select or multi_select, in order, as stored. Empty on every other kind.
modestringwritable: attendees answer it and see it (a question).readonly: organizers record it, attendees see it.secret: organizers record it, only they see it.- one of
writablereadonlysecret max_filesinteger- How many files one person may upload on a file field (1–10); 1 on every other kind.
other_choiceboolean- A select or multi_select ends in a write-in answer: what the person writes is stored as the answer itself. false on every other kind.
other_labelstring- The organizer's own word for the write-in row; empty means the standard “Other” in the buyer's language.
country_onlyarray of string- country fields only: the countries offered, as ISO 3166-1 alpha-2 codes; empty offers every country. A country field's answers are these codes too. Empty on every other kind.
country_pinnedarray of string- country fields only: the countries that head the list, in this order, as ISO 3166-1 alpha-2 codes. A pinned country is always offered. Empty on every other kind.
Responses
200The field as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The field is archived (archived); OR akindchange is refused because some of the field's answers don't fit the new kind (answers_do_not_fit); a question flow branch or another question's rule depends on this field's current answers (conditions_depend_on_answers); or the field changed since it was loaded — fetch it again and retry (field_changed).422The body was unreadable, carried an unknown field or a null, blanked the label, emptied a choice field's options, named an unknown mode, or setmax_filesoutside 1–10 or on a non-file field (invalid_body); OR akindchange is refused because the new kind is unsupported or isfileon either side (invalid_kind), or a select/multi_select target is left with no choices once its options are cleaned (choices_required).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/fields/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"label":"T-shirt size"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/fields/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"label": "T-shirt size"
}),
});
const data = await res.json();
{
"id": 12,
"key": "t-shirt-size",
"label": "T-shirt size",
"kind": "select",
"options": [
"S",
"M",
"L"
],
"scope": "person",
"mode": "writable",
"description": "For the goody bag.",
"max_files": 1,
"other_choice": false,
"other_label": "",
"country_only": [],
"country_pinned": [
"AE",
"SA",
"GB",
"IN"
],
"position": 3,
"archived": false
}
Restore an archived field
POST /fields/{id}/restore
Back into the library under its own key. When a live field already carries the archived one's label the restore is 409 name_taken unless the body gives a new label — the same question the admin's own confirm step asks.
Requires catalog.manage
Path parameters
idinteger required- The field's id, as
GET /fieldslists it.
Request body
labelstring- A new label for the field on its way back; required when a live field already carries the old one.
Response fields
kindstring- What kind of answer it takes.
- one of
texttextareaselectmulti_selectcheckboxyes_nodatenumberphonecountryfile optionsarray of string- The choices of a select or multi_select, in order, as stored. Empty on every other kind.
modestringwritable: attendees answer it and see it (a question).readonly: organizers record it, attendees see it.secret: organizers record it, only they see it.- one of
writablereadonlysecret max_filesinteger- How many files one person may upload on a file field (1–10); 1 on every other kind.
other_choiceboolean- A select or multi_select ends in a write-in answer: what the person writes is stored as the answer itself. false on every other kind.
other_labelstring- The organizer's own word for the write-in row; empty means the standard “Other” in the buyer's language.
country_onlyarray of string- country fields only: the countries offered, as ISO 3166-1 alpha-2 codes; empty offers every country. A country field's answers are these codes too. Empty on every other kind.
country_pinnedarray of string- country fields only: the countries that head the list, in this order, as ISO 3166-1 alpha-2 codes. A pinned country is always offered. Empty on every other kind.
Responses
200The restored field.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The field is not archived (not_archived), or a live field carries its label and no new one was given (name_taken).422The body was unreadable or carried an unknown field (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/fields/1/restore \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"label":"Company (2024)"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/fields/1/restore", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"label": "Company (2024)"
}),
});
const data = await res.json();
{
"id": 12,
"key": "t-shirt-size",
"label": "T-shirt size",
"kind": "select",
"options": [
"S",
"M",
"L"
],
"scope": "person",
"mode": "writable",
"description": "For the goody bag.",
"max_files": 1,
"other_choice": false,
"other_label": "",
"country_only": [],
"country_pinned": [
"AE",
"SA",
"GB",
"IN"
],
"position": 3,
"archived": false
}
Translations
List an event's translation languages
GET /events/{slug}/translations
Which languages this event offers words in, and how far each has got. Which languages it offers is the settings panel (PATCH /events/{slug}/settings/languages); this is what they say.
Requires events.view
Path parameters
slugstring required- The event's slug.
Response fields
languagesarray of TranslationLanguagelanguages[].localestringlanguages[].namestring- The language's own name. Never a flag or a country — no country owns a language.
Responses
200The languages.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/translations \
-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/translations", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"languages": [
{
"locale": "ga",
"name": "Gaeilge",
"strings": 46,
"translated": 12,
"published": 8
}
]
}
List a language's strings
GET /events/{slug}/translations/{locale}
Every translatable string with the base text it translates, the working text and what is published. Each is addressed by its REF — "event.name", "ticket_type.2.description" — the same key the hand-off spreadsheet uses, so a file a translator filled in and a body a script sends name the strings the same way. SAVING IS NOT PUBLISHING. A save writes the working text; attendees go on reading what was published until somebody publishes, which is the editor's own split and the one thing a caller could otherwise get badly wrong. The list carries the pieces of Content this event has too, which the translations EDITOR deliberately does not offer (a composer body needs a composer). Their refs are static_page.{id}.title and static_page.{id}.body, and the body is the composer's STORED FORM both ways: this endpoint is read and write, so prose would lose every link, heading and list the moment a translation was PATCHed back. Read GET /events/{slug}/pages/{id} for the words as prose.
Requires events.view
Path parameters
slugstring required- The event's slug.
localestring required- A language this event offers, as
GET /events/{slug}/translationslists it.
Response fields
Responses
200The strings.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/translations/{locale} \
-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/translations/{locale}", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"locale": "ga",
"changed": 1,
"translations": [
{
"ref": "ticket_type.2.description",
"entity": "ticket_type",
"entity_id": 2,
"field": "description",
"base": "Full weekend access",
"value": "Rochtain deireadh seachtaine",
"published": ""
}
]
}
Save a language's working text
PATCH /events/{slug}/translations/{locale}
Writes the WORKING text. A ref this body does not name is left alone; "" clears one, and the event's own language shows through once that is published. A ref this event has no string for is refused rather than ignored — a typo in a spreadsheet key is exactly the thing a caller wants told. SAVING IS NOT PUBLISHING. A save writes the working text; attendees go on reading what was published until somebody publishes, which is the editor's own split and the one thing a caller could otherwise get badly wrong. The list carries the pieces of Content this event has too, which the translations EDITOR deliberately does not offer (a composer body needs a composer). Their refs are static_page.{id}.title and static_page.{id}.body, and the body is the composer's STORED FORM both ways: this endpoint is read and write, so prose would lose every link, heading and list the moment a translation was PATCHed back. Read GET /events/{slug}/pages/{id} for the words as prose.
Requires events.manage
Path parameters
slugstring required- The event's slug.
localestring required- A language this event offers, as
GET /events/{slug}/translationslists it.
Request body
Response fields
Responses
200The strings as they now stand, with how many moved.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409unpublished_changes: astatic_page.{id}ref belongs to a page with unpublished changes in the admin. Nothing in the request was written. Publish or discard them in the admin, then try again.422valueswas absent, named a string this event does not have, or the body carried an unknown field (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/translations/{locale} \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"values":null}'
// 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/translations/{locale}", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"values": null
}),
});
const data = await res.json();
{
"locale": "ga",
"changed": 1,
"translations": [
{
"ref": "ticket_type.2.description",
"entity": "ticket_type",
"entity_id": 2,
"field": "description",
"base": "Full weekend access",
"value": "Rochtain deireadh seachtaine",
"published": ""
}
]
}
Publish a language
POST /events/{slug}/translations/{locale}/publish
The one action that changes what attendees read: it copies the working text over the published copy. A string cleared since it was published falls back to the event's own language, and its row is dropped rather than left as a husk.
Requires events.manage
Path parameters
slugstring required- The event's slug.
localestring required- A language this event offers, as
GET /events/{slug}/translationslists it.
Response fields
Responses
200The strings as they now stand.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/translations/{locale}/publish \
-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/translations/{locale}/publish", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"locale": "ga",
"changed": 1,
"translations": [
{
"ref": "ticket_type.2.description",
"entity": "ticket_type",
"entity_id": 2,
"field": "description",
"base": "Full weekend access",
"value": "Rochtain deireadh seachtaine",
"published": ""
}
]
}
Downloads
List an event's files
GET /events/{slug}/downloads
The event's file library: the documents an organiser hands out, offered through portals and ticket types. A file is addressed by its TOKEN and never by its row id — an enumerable id must not address a file.
Requires events.view
Path parameters
slugstring required- The event's slug.
Response fields
downloadsarray of Downloaddownloads[].descriptionstringdownloads[].content_typestringdownloads[].urlstring- Where the bytes are, behind the admin gate — the organiser's own copy, not a holder's link. On the root domain, where admin is; this instance's own address on a standalone account.
downloads[].created_atstring (RFC 3339) or null
Responses
200The library.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/downloads \
-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/downloads", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"downloads": [
{
"token": "1qR20rFnPEV70bqeGNLJZg",
"filename": "floor-plan.pdf",
"title": "Floor plan",
"description": "Where everything is",
"content_type": "application/pdf",
"size": 20481,
"url": "https://go.tito.io/youraccount/annual/admin/downloads/1qR20rFnPEV70bqeGNLJZg",
"created_at": "2026-09-08T11:00:00Z"
}
]
}
Upload a file
POST /events/{slug}/downloads
Takes a MULTIPART body with the bytes in file — not JSON, and not a presigned URL. The cap is 10 MB and the bytes are stored in one call, so a direct upload is the honest shape; a presigned URL would add a round trip and a second failure mode to a file small enough to send in one. ONE file per request: the admin's form takes several because a browser offers a multi-select, but a caller sending two would have no way to name them apart. A filename carrying a path is stripped to its last segment.
Requires events.manage
Path parameters
slugstring required- The event's slug.
Response fields
descriptionstringcontent_typestringurlstring- Where the bytes are, behind the admin gate — the organiser's own copy, not a holder's link. On the root domain, where admin is; this instance's own address on a standalone account.
created_atstring (RFC 3339) or null
Responses
201The stored file.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.413The file is over the 10 MB cap (too_big).422The body was not a multipart upload, carried nofilepart, carried more than one, or the file was empty (invalid_body).500Internal error.503This account has no file store configured (unavailable).507The account has used all of its file storage (storage_full). Remove files, or ask Tito support for more.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/downloads \
-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/downloads", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"token": "1qR20rFnPEV70bqeGNLJZg",
"filename": "floor-plan.pdf",
"title": "Floor plan",
"description": "Where everything is",
"content_type": "application/pdf",
"size": 20481,
"url": "https://go.tito.io/youraccount/annual/admin/downloads/1qR20rFnPEV70bqeGNLJZg",
"created_at": "2026-09-08T11:00:00Z"
}
Remove a file
DELETE /events/{slug}/downloads/{token}
The row first, then the object: a row with no object is a broken download, an object with no row is a leaked file nobody can reach, and the second is the safer failure. Removing a file also drops it from every portal it had been given to.
Requires events.manage
Path parameters
slugstring required- The event's slug.
tokenstring required- The file's token, as
GET /events/{slug}/downloadslists it.
Response fields
Responses
200Deleted.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/downloads/{token} \
-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/downloads/{token}", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"token": "1qR20rFnPEV70bqeGNLJZg",
"deleted": true
}
Get a file's details
GET /events/{slug}/downloads/{token}
One file. The bytes are at its url, behind the admin gate.
Requires events.view
Path parameters
slugstring required- The event's slug.
tokenstring required- The file's token, as
GET /events/{slug}/downloadslists it.
Response fields
descriptionstringcontent_typestringurlstring- Where the bytes are, behind the admin gate — the organiser's own copy, not a holder's link. On the root domain, where admin is; this instance's own address on a standalone account.
created_atstring (RFC 3339) or null
Responses
200The file.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/downloads/{token} \
-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/downloads/{token}", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"token": "1qR20rFnPEV70bqeGNLJZg",
"filename": "floor-plan.pdf",
"title": "Floor plan",
"description": "Where everything is",
"content_type": "application/pdf",
"size": 20481,
"url": "https://go.tito.io/youraccount/annual/admin/downloads/1qR20rFnPEV70bqeGNLJZg",
"created_at": "2026-09-08T11:00:00Z"
}
Rename a file or describe it
PATCH /events/{slug}/downloads/{token}
The name and the description, which are the only things about an uploaded file that can change after the fact: the bytes are the file and the uploaded filename is its identity underneath, so correcting either of those means uploading again.
Requires events.manage
Path parameters
slugstring required- The event's slug.
tokenstring required- The file's token, as
GET /events/{slug}/downloadslists it.
Request body
descriptionstring
Response fields
descriptionstringcontent_typestringurlstring- Where the bytes are, behind the admin gate — the organiser's own copy, not a holder's link. On the root domain, where admin is; this instance's own address on a standalone account.
created_atstring (RFC 3339) or null
Responses
200The file as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body was unreadable or carried an unknown field (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/downloads/{token} \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"title":"Hall layout"}'
// 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/downloads/{token}", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"title": "Hall layout"
}),
});
const data = await res.json();
{
"token": "1qR20rFnPEV70bqeGNLJZg",
"filename": "floor-plan.pdf",
"title": "Floor plan",
"description": "Where everything is",
"content_type": "application/pdf",
"size": 20481,
"url": "https://go.tito.io/youraccount/annual/admin/downloads/1qR20rFnPEV70bqeGNLJZg",
"created_at": "2026-09-08T11:00:00Z"
}
Invitations
List invitation lists
GET /events/{slug}/invitation-lists
An invitation list is a set of ticket types the organizer hands out by invitation; an invitation on it is one person's bearer link (url) to claim up to max_tickets of them, optionally narrowed to some of the list's types. Lists a portal or an application page owns for itself are not the organizer's and are not here. Invitation writes keep no history rows, as the admin's don't. Archived lists are included and marked.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
Response fields
invitation_listsarray of InvitationListinvitation_lists[].namestringinvitation_lists[].default_edit_detailsboolean- Whether a new invitation's person may change the name and email typed for them.
invitation_lists[].claim_deadlinestring (RFC 3339) or null- When the list's invitations stop working; null for none.
invitation_lists[].archivedboolean- Put away; no new invitations, existing ones keep working until their deadline.
invitation_lists[].created_atstring (RFC 3339) or null
Responses
200Lists.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/invitation-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/invitation-lists", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"invitation_lists": [
{
"id": 5,
"name": "Speakers",
"ticket_type_ids": [
7,
8
],
"default_max_tickets": 2,
"default_edit_details": false,
"claim_deadline": null,
"archived": false,
"created_at": "2026-09-07T21:20:00Z"
}
]
}
Create an invitation list
POST /events/{slug}/invitation-lists
A list offering one or more of the event's ticket types. default_max_tickets (1–50, default 1) and default_edit_details are what each new invitation inherits and are fixed at creation, as on the form; the claim deadline is set with a PATCH.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
Request body
namestring
Response fields
namestringdefault_edit_detailsboolean- Whether a new invitation's person may change the name and email typed for them.
created_atstring (RFC 3339) or null
Responses
201The new list.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body was unreadable or carried an unknown field, the name was blank, no ticket type was given or one is not this event's, ordefault_max_ticketsis outside 1–50 (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitation-lists \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Speakers"}'
// 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/invitation-lists", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Speakers"
}),
});
const data = await res.json();
{
"id": 5,
"name": "Speakers",
"ticket_type_ids": [
7,
8
],
"default_max_tickets": 2,
"default_edit_details": false,
"claim_deadline": null,
"archived": false,
"created_at": "2026-09-07T21:20:00Z"
}
Get an invitation list
GET /events/{slug}/invitation-lists/{id}
One organizer-owned list.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation list's id, as
GET /events/{slug}/invitation-listslists it.
Response fields
namestringdefault_edit_detailsboolean- Whether a new invitation's person may change the name and email typed for them.
created_atstring (RFC 3339) or null
Responses
200The list.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/invitation-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/invitation-lists/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 5,
"name": "Speakers",
"ticket_type_ids": [
7,
8
],
"default_max_tickets": 2,
"default_edit_details": false,
"claim_deadline": null,
"archived": false,
"created_at": "2026-09-07T21:20:00Z"
}
Update an invitation list
PATCH /events/{slug}/invitation-lists/{id}
The list form's own fields: name, claim_deadline (RFC 3339, or "" for none), and when sent, ticket_type_ids replaces what the list offers (at least one). The two defaults are fixed at creation.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation list's id, as
GET /events/{slug}/invitation-listslists it.
Request body
namestring
Response fields
namestringdefault_edit_detailsboolean- Whether a new invitation's person may change the name and email typed for them.
created_atstring (RFC 3339) or null
Responses
200The list as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422A blank name, an empty or foreignticket_type_ids, a default sent after creation (invalid_body), or a malformed deadline (invalid_dates).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitation-lists/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Speakers"}'
// 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/invitation-lists/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Speakers"
}),
});
const data = await res.json();
{
"id": 5,
"name": "Speakers",
"ticket_type_ids": [
7,
8
],
"default_max_tickets": 2,
"default_edit_details": false,
"claim_deadline": null,
"archived": false,
"created_at": "2026-09-07T21:20:00Z"
}
Archive an invitation list
POST /events/{slug}/invitation-lists/{id}/archive
Puts the list away. Its invitations keep working until their deadline; there is no way back, as on the admin.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation list's id, as
GET /events/{slug}/invitation-listslists it.
Response fields
namestringdefault_edit_detailsboolean- Whether a new invitation's person may change the name and email typed for them.
created_atstring (RFC 3339) or null
Responses
200The list, archived.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitation-lists/1/archive \
-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/invitation-lists/1/archive", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 5,
"name": "Speakers",
"ticket_type_ids": [
7,
8
],
"default_max_tickets": 2,
"default_edit_details": false,
"claim_deadline": null,
"archived": false,
"created_at": "2026-09-07T21:20:00Z"
}
Invite someone
POST /events/{slug}/invitation-lists/{id}/invitations
One invitation on the list: a named person with an email, or with neither an open link anyone can claim (then edit_details is on). max_tickets defaults to the list's; ticket_type_ids narrows the list's offer for this person; deadline overrides the list's. The email is checked for syntax only. Nothing is emailed — POST /invitations/{id}/send does that, or send the url through your own channel.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation list's id, as
GET /events/{slug}/invitation-listslists it.
Request body
namestringccarray of string- Addresses to copy in, at most 10, each checked for syntax only. Needs the invitation's own email. The person's own address is dropped. On a PATCH the list replaces what was there, and
[]clears it.
Response fields
email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. email_history[].idintegeremail_history[].message_idintegeremail_history[].subjectstringemail_history[].recipientstringemail_history[].ccstringemail_history[].statusstringemail_history[].created_atintegerccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
revokedbooleancreated_atstring (RFC 3339) or nullduplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
answered_for.atstring (RFC 3339)
Responses
201The invitation, with its url.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body was unreadable or carried an unknown field, the email is not an address,max_ticketsis outside 1–50, a ticket type is not on the list, orlist_idwas sent (invalid_body); a malformed deadline (invalid_dates).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitation-lists/1/invitations \
-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/events/annual-conf/invitation-lists/1/invitations", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Ada Lovelace"
}),
});
const data = await res.json();
{
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
}
Get invitation list prices
GET /events/{slug}/invitation-lists/{id}/prices
Active offered ticket types only, respecting a recipient’s subset. Lists use the event currency. Recipients use their invitation currency. Extra-currency invitations inherit public overlay prices, bypassing list overrides. Portal-owned and request-owned lists are not accessible.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
idinteger required
Response fields
currencystringpricesarray of objectprices[].ticket_type_idintegerprices[].namestringprices[].full_price_centsintegerprices[].inherited_price_centsintegerprices[].price_centsintegerprices[].effective_price_centsinteger
Responses
200Current offered ticket prices and inheritance in the reported currency401No 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/invitation-lists/1/prices \
-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/invitation-lists/1/prices", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"currency": "eur",
"prices": [
{
"ticket_type_id": 1,
"name": "Ada Lovelace",
"full_price_cents": 14900,
"inherited_price_cents": 14900,
"price_cents": 14900,
"effective_price_cents": 14900
}
]
}
Update invitation list prices
PATCH /events/{slug}/invitation-lists/{id}/prices
Active offered ticket types only, respecting a recipient’s subset. Lists use the event currency. Recipients use their invitation currency. Extra-currency invitations inherit public overlay prices, bypassing list overrides. Portal-owned and request-owned lists are not accessible. Atomically change only supplied entries. Null removes an override, zero is free, and omission preserves the current price. An empty array is a no-op. Prices cannot exceed the ticket’s full public price or the UI currency amount limit. This does not send email or reprice existing orders.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Request body
pricesarray of object requiredprices[].ticket_type_idinteger requiredprices[].price_centsinteger required
Response fields
currencystringpricesarray of objectprices[].ticket_type_idintegerprices[].namestringprices[].full_price_centsintegerprices[].inherited_price_centsintegerprices[].price_centsintegerprices[].effective_price_centsinteger
Responses
200Current offered ticket prices and inheritance in the reported currency401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The recipient or list changed since lookup. Read the prices and retry.422Invalid, duplicate, or unavailable ticket type or price500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitation-lists/1/prices \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"prices":[{"ticket_type_id":1,"price_cents":14900}]}'
// 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/invitation-lists/1/prices", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"prices": [
{
"ticket_type_id": 1,
"price_cents": 14900
}
]
}),
});
const data = await res.json();
{
"currency": "eur",
"prices": [
{
"ticket_type_id": 1,
"name": "Ada Lovelace",
"full_price_cents": 14900,
"inherited_price_cents": 14900,
"price_cents": 14900,
"effective_price_cents": 14900
}
]
}
Preview invitation list emails
GET /events/{slug}/invitation-lists/{id}/send
Up to 500 open, non-revoked invitations with email addresses. Unsent means never emailed; remind means previously emailed. Expired invitations remain eligible. Review recipients and deadlines before sending. Follow next_after when has_more is true.
Requires invitations.view
Path parameters
slugstring requiredidinteger required
Query parameters
modestringafterinteger
Response fields
invitationsarray of Invitationinvitations[].email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. invitations[].email_history[].idintegerinvitations[].email_history[].message_idintegerinvitations[].email_history[].subjectstringinvitations[].email_history[].recipientstringinvitations[].email_history[].ccstringinvitations[].email_history[].statusstringinvitations[].email_history[].created_atintegerinvitations[].urlstring- The link the person claims with — a bearer credential; sending it IS inviting them.
invitations[].ccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
invitations[].edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
invitations[].statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected invitations[].statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted invitations[].ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
invitations[].deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
invitations[].revokedbooleaninvitations[].created_atstring (RFC 3339) or nullinvitations[].duplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
invitations[].anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
invitations[].answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
invitations[].answered_for.bystring- The teammate who answered, by email address, or the API key that did.
invitations[].answered_for.atstring (RFC 3339)invitations[].answered_for.reasonstring- What they wrote as the reason. Empty when they wrote none.
has_moreboolean
Responses
200Eligible invitation preview in ascending id order.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such organizer invitation list or invitation.422Invalid mode or cursor.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitation-lists/1/send \
-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/invitation-lists/1/send", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"invitations": [
{
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
}
],
"mode": "unsent",
"has_more": true,
"next_after": 1
}
Send invitation list emails
POST /events/{slug}/invitation-lists/{id}/send
Validate every recipient belongs to this organizer list before sending. Ineligible or recently claimed recipients are skipped. Each claimed invitation's email is queued and goes out in the background, surviving a restart; a delivery that then fails releases its claim, so the invitation reads as unsent again. An invitation whose email is still queued is never queued twice. No idempotency-key support: reminders may send again after the two-minute cooldown once nothing is queued. Check send timestamps before retrying an uncertain result. Put mode in the POST body; query parameters are refused.
Requires invitations.manage
Path parameters
slugstring requiredidinteger required
Request body
Response fields
Responses
200Batch preview or delivery results. Queued means the email is on its way: it goes out in the background, and an invitation counts as sent from the moment it is queued. Logged means recorded without sending. A skipped invitation whose email is still waiting to go out from an earlier send has the reasonqueued.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such organizer invitation list or invitation.422Invalid mode, confirmation, cursor or invitation ids.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitation-lists/1/send \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"confirmed":true,"invitation_ids":[1]}'
// 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/invitation-lists/1/send", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"confirmed": true,
"invitation_ids": [
1
]
}),
});
const data = await res.json();
{
"results": [
{
"id": 1,
"delivery": "queued",
"reason": "ineligible"
}
]
}
Clear InvitationList RSVP settings
DELETE /events/{slug}/invitation-lists/{id}/settings
Remove every local RSVP setting override and reveal inherited values. Does not change question rules or prices.
Requires invitations.manage
Path parameters
slugstring requiredidinteger required
Response fields
Responses
200Explicit local overrides and merged effective settings.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such organizer-owned list or invitation in this event.500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitation-lists/1/settings \
-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/invitation-lists/1/settings", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"overrides": {},
"effective": {}
}
Get InvitationList RSVP settings
GET /events/{slug}/invitation-lists/{id}/settings
Read local overrides and effective values inherited from built-in defaults, event, list, and (for an invitation) its own overrides.
Requires invitations.view
Path parameters
slugstring requiredidinteger required
Response fields
Responses
200Explicit local overrides and merged effective settings.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such organizer-owned list or invitation in this event.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitation-lists/1/settings \
-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/invitation-lists/1/settings", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"overrides": {},
"effective": {}
}
Update InvitationList RSVP settings
PATCH /events/{slug}/invitation-lists/{id}/settings
Patch only the supplied settings. Omitted overrides remain unchanged, including refund timing. inherit removes a local override. Empty strings are refused. All validation and writes are atomic. Does not change field question rules or prices.
Requires invitations.manage
Path parameters
slugstring requiredidinteger required
Request body
settingsobject requiredsettings.refund_deadlinestring- inherit removes the local override. Otherwise use RFC3339 or event-local YYYY-MM-DDTHH:MM.
settings.refund_before_hoursstring- inherit removes the local override. Otherwise use a nonnegative integer number of hours.
Response fields
Responses
200Explicit local overrides and merged effective settings.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such organizer-owned list or invitation in this event.422Unknown setting or invalid value.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitation-lists/1/settings \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"settings":{"primary_name":"off"}}'
// 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/invitation-lists/1/settings", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"settings": {
"primary_name": "off"
}
}),
});
const data = await res.json();
{
"overrides": {},
"effective": {}
}
Get invitation list question rules
GET /events/{slug}/invitation-lists/{id}/settings/fields
Read selected, inherited, and effective appearances for active person fields. ticket_types names the ticket types offered here that ask each question. A ticket type's question is asked on the RSVP unless a scope turns it off, and effective_* includes it. Only organizer-owned records in the named event are accessible.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
idinteger required
Response fields
fieldsarray of objectfields[].field_idintegerfields[].ticket_typesarray of object- The ticket types offered at this scope that ask this question, so an RSVP for one of them asks it unless this scope turns it off. Empty for a question only the invitation asks.
fields[].ticket_types[].idintegerfields[].ticket_types[].namestringfields[].ticket_types[].requiredbooleanfields[].keystringfields[].labelstringfields[].kindstring
Responses
200OK401No 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/invitation-lists/1/settings/fields \
-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/invitation-lists/1/settings/fields", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"fields": [
{
"field_id": 1,
"ticket_types": [
{
"id": 1,
"name": "Ada Lovelace",
"required": true
}
],
"key": "example",
"label": "example",
"kind": "ticket",
"primary": "optional",
"guest": "optional",
"inherited_primary": "optional",
"inherited_guest": "optional",
"effective_primary": "optional",
"effective_guest": "optional"
}
]
}
Update invitation list question rules
PATCH /events/{slug}/invitation-lists/{id}/settings/fields
Atomically update supplied field and audience cells. Omitted cells stay unchanged. optional is refused for a question a ticket type requires. default removes the local override. off without an inherited rule removes a redundant override. Empty fields is a no-op. Null, duplicate cells, and inactive fields are refused.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Request body
Response fields
fieldsarray of objectfields[].field_idintegerfields[].ticket_typesarray of object- The ticket types offered at this scope that ask this question, so an RSVP for one of them asks it unless this scope turns it off. Empty for a question only the invitation asks.
fields[].ticket_types[].idintegerfields[].ticket_types[].namestringfields[].ticket_types[].requiredbooleanfields[].keystringfields[].labelstringfields[].kindstring
Responses
200OK401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Each cell needs field_id, audience (primary or guest), and appearance. Each field and audience pair must be unique.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitation-lists/1/settings/fields \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"fields":[{"field_id":1,"audience":"primary","appearance":"optional"}]}'
// 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/invitation-lists/1/settings/fields", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"fields": [
{
"field_id": 1,
"audience": "primary",
"appearance": "optional"
}
]
}),
});
const data = await res.json();
{
"fields": [
{
"field_id": 1,
"ticket_types": [
{
"id": 1,
"name": "Ada Lovelace",
"required": true
}
],
"key": "example",
"label": "example",
"kind": "ticket",
"primary": "optional",
"guest": "optional",
"inherited_primary": "optional",
"inherited_guest": "optional",
"effective_primary": "optional",
"effective_guest": "optional"
}
]
}
List invitations
GET /events/{slug}/invitations
Every invitation on the organizer's lists, newest first, with each one's status and url; ?list=<id> narrows to one list.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
Query parameters
listinteger- An invitation list's id.
Response fields
invitationsarray of Invitationinvitations[].email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. invitations[].email_history[].idintegerinvitations[].email_history[].message_idintegerinvitations[].email_history[].subjectstringinvitations[].email_history[].recipientstringinvitations[].email_history[].ccstringinvitations[].email_history[].statusstringinvitations[].email_history[].created_atintegerinvitations[].urlstring- The link the person claims with — a bearer credential; sending it IS inviting them.
invitations[].ccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
invitations[].edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
invitations[].statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected invitations[].statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted invitations[].ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
invitations[].deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
invitations[].revokedbooleaninvitations[].created_atstring (RFC 3339) or nullinvitations[].duplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
invitations[].anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
invitations[].answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
invitations[].answered_for.bystring- The teammate who answered, by email address, or the API key that did.
invitations[].answered_for.atstring (RFC 3339)invitations[].answered_for.reasonstring- What they wrote as the reason. Empty when they wrote none.
Responses
200Invitations.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422listis not an id (invalid_body).500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations \
-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/invitations", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"invitations": [
{
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
}
]
}
Get an invitation
GET /events/{slug}/invitations/{id}
One invitation, with its status and url.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation's id, as
GET /events/{slug}/invitationslists it.
Response fields
email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. email_history[].idintegeremail_history[].message_idintegeremail_history[].subjectstringemail_history[].recipientstringemail_history[].ccstringemail_history[].statusstringemail_history[].created_atintegerccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
revokedbooleancreated_atstring (RFC 3339) or nullduplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
answered_for.atstring (RFC 3339)
Responses
200The invitation.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/invitations/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/invitations/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
}
Update an invitation
PATCH /events/{slug}/invitations/{id}
The invitation form's own fields. list_id moves it to another of the organizer's lists, which drops its narrowing and per-person prices, as the form's move does. edit_details is forced on when the email is blanked. The form offers to tell the person about an extra place; a caller decides that for itself, so this only writes.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation's id, as
GET /events/{slug}/invitationslists it.
Request body
namestringccarray of string- Addresses to copy in, at most 10, each checked for syntax only. Needs the invitation's own email. The person's own address is dropped. On a PATCH the list replaces what was there, and
[]clears it.
Response fields
email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. email_history[].idintegeremail_history[].message_idintegeremail_history[].subjectstringemail_history[].recipientstringemail_history[].ccstringemail_history[].statusstringemail_history[].created_atintegerccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
revokedbooleancreated_atstring (RFC 3339) or nullduplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
answered_for.atstring (RFC 3339)
Responses
200The invitation as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422As for create;list_idnot one of the organizer's lists (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1 \
-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/events/annual-conf/invitations/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Ada Lovelace"
}),
});
const data = await res.json();
{
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
}
Anonymize an invitee
POST /events/{slug}/invitations/{id}/anonymize
Remove the invitee from an invitation: their name, email address and the addresses copied in, their guests' names and addresses, the copy of the invitation email, every answer, the buyer details on orders made through it and the person on every ticket claimed through it. It happens at once and cannot be undone, the API's copy of the admin's Anonymize confirm step, which runs the same code. What stays: the record itself with its reference, status and place in every count, what was bought and paid and when, the payment reference, and every invoice, receipt and credit note already issued, exactly as issued. Copies of the person in sent emails, the record's history, its webhook deliveries, workflow runs and the API request log are cleared too. What Tito cannot reach is named in outside_tito: the payment provider's own record, connected services the person was sent to, and whatever your webhooks already delivered. Every call writes a line to the data-protection log (GET /data-protection-log) that names the record's reference and never the person. The link tags its orders arrived with go too, ad click ids and your own tags included, except utm_* campaign tags, which stay so the order still counts toward the link that sold it. Send no request body; any body is ignored. An order still waiting for a bank transfer is canceled first, so a payment that arrives later cannot fulfil it; one whose tickets were already issued is left as it is.
Requires erasure
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation's id, as
GET /events/{slug}/invitationslists it.
Response fields
refstring- The record's reference: a ticket, order, customer, waitlist entry or ticket request ref, or the invitation's id.
anonymized_atstring (RFC 3339)removedobjectremoved.emailsinteger- Sent emails whose address and words were removed. The record that each was sent stays.
removed.link_tagsinteger- Orders whose link tags were removed: every tag that isn't a
utm_*campaign tag, ad click ids included. removed.interestboolean- Their email address was taken off the mailing list, where they signed up to hear when tickets go on sale.
removed.cardboolean- A card saved with a ticket request this withdrew, which Stripe was asked to delete.
keptobjectkept.other_recordsinteger- Other orders, tickets and invitations with the same email address, which are not changed.
kept.customerstring or null- The customer record an anonymized order is linked to, which is not changed.
kept.campaign_tagsinteger- Orders whose
utm_*campaign tags were kept, so they still count toward the link that sold them. kept.orderstring or null- The order a waitlist place was taken with, or that a ticket request's invitation was claimed with, which is not changed.
kept.invitationinteger or null- The invitation a ticket request was approved into, which is not changed.
outside_titoobjectoutside_tito.payment_providerstring or null- The provider that keeps its own record of the payment, with the buyer's name and email address.
- one of
StripePayPal<nil> outside_tito.servicesarray of string- Connected services the person's details were sent to, which keep their own copy.
outside_tito.cardboolean- A card saved with a ticket request that was already decided, which Stripe keeps.
Responses
200The invitee is anonymized.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The person on this record has already been anonymized (already_anonymized). Nothing changes.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/anonymize \
-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/invitations/1/anonymize", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"anonymized": true,
"kind": "ticket",
"ref": "H7K2PX",
"anonymized_at": "2026-10-14T09:00:00Z",
"removed": {
"tickets": 1,
"orders": 1,
"quotes": 1,
"answers": 1,
"files": 1,
"emails": 1,
"contact": true,
"waitlist": true,
"ticket_request": true,
"link_tags": 1,
"interest": true,
"card": true
},
"kept": {
"documents": 1,
"other_records": 1,
"customer": "example",
"campaign_tags": 1,
"order": "H7K2PX",
"ticket": "T9QW4M",
"invitation": 1
},
"outside_tito": {
"payment_provider": "Stripe",
"services": [
"example"
],
"webhooks": true,
"card": true
}
}
Approve an invitation waiting on approval
POST /events/{slug}/invitations/{id}/approve
Opens an invitation that is waiting on approval (awaiting_approval) or was turned down (rejected), so its link can be claimed, and emails it with the account's stock invitation wording, as the admin's Approve button does. No request body. Answers the invitation and how the email went: sent, logged (no mail transport on this instance), failed, or none (no email address, so share its url yourself).
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation's id, as
GET /events/{slug}/invitationslists it.
Response fields
invitation.email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. invitation.email_history[].idintegerinvitation.email_history[].message_idintegerinvitation.email_history[].subjectstringinvitation.email_history[].recipientstringinvitation.email_history[].ccstringinvitation.email_history[].statusstringinvitation.email_history[].created_atintegerinvitation.urlstring- The link the person claims with — a bearer credential; sending it IS inviting them.
invitation.ccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
invitation.edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
invitation.statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected invitation.statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted invitation.ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
invitation.deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
invitation.revokedbooleaninvitation.created_atstring (RFC 3339) or nullinvitation.duplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
invitation.anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
invitation.answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
invitation.answered_for.bystring- The teammate who answered, by email address, or the API key that did.
invitation.answered_for.atstring (RFC 3339)
Responses
200Approved.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Not waiting on approval or turned down, or revoked (not_decidable).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/approve \
-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/invitations/1/approve", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"invitation": {
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
},
"delivery": "sent"
}
Cancel the tickets a claimed invitation issued
POST /events/{slug}/invitations/{id}/cancel-tickets
Every ticket the invitation's orders hold stops working, their places go back on sale, and the invitation is marked canceled. It is the admin's Cancel their tickets, and the same act as the invitee canceling their own RSVP. Revoking only stops the link. This is what takes the tickets.
When the invitation was paid for, refund is required: true sends back what is left on each payment, on the rail it came in on, and false keeps the money. A free invitation ignores it. email (default false) sends the invitee the RSVP cancellation email.
An invitation with no working tickets answers 409 not_cancelable. One already canceled answers 200 with already: true, so a retry is safe. If any payment cannot be refunded, nothing is canceled and the answer is 503 refund_unavailable. A refund the provider refuses answers 502 refund_failed, and the tickets still work.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation's id, as
GET /events/{slug}/invitationslists it.
Request body
refundboolean- Send the money back (true) or keep it (false). Required when the invitation was paid for.
Response fields
Responses
200The tickets no longer work.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The invitation has no working tickets (not_cancelable).422A paid invitation with norefundanswer, or a body that isn't the shape above (invalid_body).500Internal error.502The refund did not go through (refund_failed).503A payment can't be refunded right now, so nothing was canceled (refund_unavailable).
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/cancel-tickets \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"refund":true}'
// 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/invitations/1/cancel-tickets", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"refund": true
}),
});
const data = await res.json();
{
"canceled": true,
"id": 1,
"tickets": 1,
"refunded": true,
"already": true
}
Claim an invitation for the invitee
POST /events/{slug}/invitations/{id}/claim
The admin's Claim for them, for somebody who said yes another way, like replying to the email. Their tickets are issued and the invitation is marked yes, exactly as if they had claimed it on their own link: the first ticket is theirs, and any further places go to whoever was pre-filled for them, or nobody yet.
It goes past what would stop the invitee. A ticket type with too few places left is claimed past its limit, a passed claim deadline is ignored, and a priced ticket is issued free of charge. The invitation's history records each of these, with reason and who made the call.
ticket_type_id is needed only when the invitation offers more than one type, and quantity can be up to the invitation's max_tickets, which is also its default. email (default false) sends the invitee the RSVP confirmation with their tickets.
A revoked invitation, one already claimed, one waiting on approval, a portal's, or one with no email address answers 409 not_answerable.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation's id, as
GET /events/{slug}/invitationslists it.
Request body
ticket_type_idinteger- The ticket type to issue. Required when the invitation offers more than one.
quantityinteger- How many tickets, up to the invitation's max_tickets. Defaults to max_tickets, everything the invitation offers.
reasonstring- Why, for your team. Kept in the invitation's history and shown on its page in the admin.
Response fields
invitation.email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. invitation.email_history[].idintegerinvitation.email_history[].message_idintegerinvitation.email_history[].subjectstringinvitation.email_history[].recipientstringinvitation.email_history[].ccstringinvitation.email_history[].statusstringinvitation.email_history[].created_atintegerinvitation.urlstring- The link the person claims with — a bearer credential; sending it IS inviting them.
invitation.ccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
invitation.edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
invitation.statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected invitation.statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted invitation.ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
invitation.deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
invitation.revokedbooleaninvitation.created_atstring (RFC 3339) or nullinvitation.duplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
invitation.anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
invitation.answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
invitation.answered_for.bystring- The teammate who answered, by email address, or the API key that did.
invitation.answered_for.atstring (RFC 3339)
Responses
200Claimed.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The invitation can't be claimed (not_answerable), or the chosen ticket type left it (choice_gone).422A ticket type is needed and missing, the quantity is out of range, the reason is too long, or the body isn't the shape above (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/claim \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"ticket_type_id":1}'
// 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/invitations/1/claim", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"ticket_type_id": 1
}),
});
const data = await res.json();
{
"invitation": {
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
},
"order_ref": "H7K2PX",
"tickets": 1,
"emailed": true
}
Decline an invitation for the invitee
POST /events/{slug}/invitations/{id}/decline
The admin's Decline for them, for somebody who said no another way. An open invitation is marked no, as if they had declined it on their own link. Nothing is emailed, and the link keeps working, so they can still claim it while places are left. reason is kept in the invitation's history.
Only an open invitation can be declined. Anything else answers 409 not_answerable.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation's id, as
GET /events/{slug}/invitationslists it.
Request body
reasonstring- Why, for your team. Kept in the invitation's history and shown on its page in the admin.
Response fields
email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. email_history[].idintegeremail_history[].message_idintegeremail_history[].subjectstringemail_history[].recipientstringemail_history[].ccstringemail_history[].statusstringemail_history[].created_atintegerccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
revokedbooleancreated_atstring (RFC 3339) or nullduplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
answered_for.atstring (RFC 3339)
Responses
200Declined.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The invitation isn't open (not_answerable).422The reason is too long, or the body isn't the shape above (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/decline \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"reason":"example"}'
// 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/invitations/1/decline", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"reason": "example"
}),
});
const data = await res.json();
{
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
}
Get invitation prices
GET /events/{slug}/invitations/{id}/prices
Active offered ticket types only, respecting a recipient’s subset. Lists use the event currency. Recipients use their invitation currency. Extra-currency invitations inherit public overlay prices, bypassing list overrides. Portal-owned and request-owned lists are not accessible.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
idinteger required
Response fields
currencystringpricesarray of objectprices[].ticket_type_idintegerprices[].namestringprices[].full_price_centsintegerprices[].inherited_price_centsintegerprices[].price_centsintegerprices[].effective_price_centsinteger
Responses
200Current offered ticket prices and inheritance in the reported currency401No 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/invitations/1/prices \
-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/invitations/1/prices", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"currency": "eur",
"prices": [
{
"ticket_type_id": 1,
"name": "Ada Lovelace",
"full_price_cents": 14900,
"inherited_price_cents": 14900,
"price_cents": 14900,
"effective_price_cents": 14900
}
]
}
Update invitation prices
PATCH /events/{slug}/invitations/{id}/prices
Active offered ticket types only, respecting a recipient’s subset. Lists use the event currency. Recipients use their invitation currency. Extra-currency invitations inherit public overlay prices, bypassing list overrides. Portal-owned and request-owned lists are not accessible. Atomically change only supplied entries. Null removes an override, zero is free, and omission preserves the current price. An empty array is a no-op. Prices cannot exceed the ticket’s full public price or the UI currency amount limit. This does not send email or reprice existing orders.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Request body
pricesarray of object requiredprices[].ticket_type_idinteger requiredprices[].price_centsinteger required
Response fields
currencystringpricesarray of objectprices[].ticket_type_idintegerprices[].namestringprices[].full_price_centsintegerprices[].inherited_price_centsintegerprices[].price_centsintegerprices[].effective_price_centsinteger
Responses
200Current offered ticket prices and inheritance in the reported currency401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The recipient or list changed since lookup. Read the prices and retry.422Invalid, duplicate, or unavailable ticket type or price500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/prices \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"prices":[{"ticket_type_id":1,"price_cents":14900}]}'
// 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/invitations/1/prices", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"prices": [
{
"ticket_type_id": 1,
"price_cents": 14900
}
]
}),
});
const data = await res.json();
{
"currency": "eur",
"prices": [
{
"ticket_type_id": 1,
"name": "Ada Lovelace",
"full_price_cents": 14900,
"inherited_price_cents": 14900,
"price_cents": 14900,
"effective_price_cents": 14900
}
]
}
Reinstate an invitation
POST /events/{slug}/invitations/{id}/reinstate
The link works again. No request body.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation's id, as
GET /events/{slug}/invitationslists it.
Response fields
email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. email_history[].idintegeremail_history[].message_idintegeremail_history[].subjectstringemail_history[].recipientstringemail_history[].ccstringemail_history[].statusstringemail_history[].created_atintegerccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
revokedbooleancreated_atstring (RFC 3339) or nullduplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
answered_for.atstring (RFC 3339)
Responses
200The invitation.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/reinstate \
-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/invitations/1/reinstate", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
}
Turn down an invitation waiting on approval
POST /events/{slug}/invitations/{id}/reject
Turns down an invitation that is waiting on approval: awaiting_approval becomes rejected. Nothing is sent, and its link says they didn't get a place. It can still be approved later. No request body.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation's id, as
GET /events/{slug}/invitationslists it.
Response fields
email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. email_history[].idintegeremail_history[].message_idintegeremail_history[].subjectstringemail_history[].recipientstringemail_history[].ccstringemail_history[].statusstringemail_history[].created_atintegerccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
revokedbooleancreated_atstring (RFC 3339) or nullduplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
answered_for.atstring (RFC 3339)
Responses
200The invitation.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Not waiting on approval, or revoked (not_decidable).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/reject \
-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/invitations/1/reject", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
}
Revoke an invitation
POST /events/{slug}/invitations/{id}/revoke
The link stops working; the row and its history stay. No request body.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation's id, as
GET /events/{slug}/invitationslists it.
Response fields
email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. email_history[].idintegeremail_history[].message_idintegeremail_history[].subjectstringemail_history[].recipientstringemail_history[].ccstringemail_history[].statusstringemail_history[].created_atintegerccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
revokedbooleancreated_atstring (RFC 3339) or nullduplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
answered_for.atstring (RFC 3339)
Responses
200The invitation.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/revoke \
-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/invitations/1/revoke", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
}
Email an invitation
POST /events/{slug}/invitations/{id}/send
Emails the invitation to its address, with the account's own stock wording (the reminder wording once it has been sent before) unless the body gives a subject and body, from the event's sender unless the body chooses another (email_sender_id). Refuses a revoked invitation, one with no email, and one sent in the last two minutes — the admin's own buttons' rules. Answers the invitation and how the delivery went: sent, logged (no mail transport on this instance) or failed.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The invitation's id, as
GET /events/{slug}/invitationslists it.
Request body
subjectstringpreviewstring- The line an inbox shows after the subject. Plain words; never filled in from the stock wording.
email_sender_address_idintegeremail_sender_address_idis optional. A positive ID selects that exact address from theaddressesreturned byGET /email-senders. Its sender must be live; a saved message can keep its unchanged choice while the sender is pending. If both IDs are positive, the address must belong toemail_sender_id, or the request returns HTTP 422invalid_body. Address ID0follows the event unless a positiveemail_sender_idselects a domain. Omitting both fields from a messagePATCHpreserves its choice. A domain-onlyPATCHrepeating the current domain also preserves a selected address. To select the domain explicitly, send its ID with address ID0.email_sender_idinteger- The sender to send from: one of the account's live senders (
GET /email-senders). 0 or left out sends from the event's sender. Any other id is refused with422 invalid_body. A chosen sender that stops being live before the email goes is replaced by the event's sender.
Response fields
invitation.email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. invitation.email_history[].idintegerinvitation.email_history[].message_idintegerinvitation.email_history[].subjectstringinvitation.email_history[].recipientstringinvitation.email_history[].ccstringinvitation.email_history[].statusstringinvitation.email_history[].created_atintegerinvitation.urlstring- The link the person claims with — a bearer credential; sending it IS inviting them.
invitation.ccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
invitation.edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
invitation.statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected invitation.statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted invitation.ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
invitation.deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
invitation.revokedbooleaninvitation.created_atstring (RFC 3339) or nullinvitation.duplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
invitation.anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
invitation.answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
invitation.answered_for.bystring- The teammate who answered, by email address, or the API key that did.
invitation.answered_for.atstring (RFC 3339)
Responses
200Sent.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Revoked (revoked), waiting on approval or turned down (not_claimable), sent within the last two minutes (recently_sent), or with an email from a batch send still waiting to go out (queued).422The invitation has no email address, the body was unreadable, oremail_sender_idis not one of the account's live senders (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/send \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"subject":"You'\''re invited to Annual Conference"}'
// 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/invitations/1/send", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"subject": "You're invited to Annual Conference"
}),
});
const data = await res.json();
{
"invitation": {
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
},
"delivery": "sent"
}
Clear Invitation RSVP settings
DELETE /events/{slug}/invitations/{id}/settings
Remove every local RSVP setting override and reveal inherited values. Does not change question rules or prices.
Requires invitations.manage
Path parameters
slugstring requiredidinteger required
Response fields
Responses
200Explicit local overrides and merged effective settings.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such organizer-owned list or invitation in this event.500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/settings \
-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/invitations/1/settings", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"overrides": {},
"effective": {}
}
Get Invitation RSVP settings
GET /events/{slug}/invitations/{id}/settings
Read local overrides and effective values inherited from built-in defaults, event, list, and (for an invitation) its own overrides.
Requires invitations.view
Path parameters
slugstring requiredidinteger required
Response fields
Responses
200Explicit local overrides and merged effective settings.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such organizer-owned list or invitation in this event.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/settings \
-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/invitations/1/settings", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"overrides": {},
"effective": {}
}
Update Invitation RSVP settings
PATCH /events/{slug}/invitations/{id}/settings
Patch only the supplied settings. Omitted overrides remain unchanged, including refund timing. inherit removes a local override. Empty strings are refused. All validation and writes are atomic. Does not change field question rules or prices.
Requires invitations.manage
Path parameters
slugstring requiredidinteger required
Request body
settingsobject requiredsettings.refund_deadlinestring- inherit removes the local override. Otherwise use RFC3339 or event-local YYYY-MM-DDTHH:MM.
settings.refund_before_hoursstring- inherit removes the local override. Otherwise use a nonnegative integer number of hours.
Response fields
Responses
200Explicit local overrides and merged effective settings.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such organizer-owned list or invitation in this event.422Unknown setting or invalid value.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/settings \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"settings":{"primary_name":"off"}}'
// 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/invitations/1/settings", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"settings": {
"primary_name": "off"
}
}),
});
const data = await res.json();
{
"overrides": {},
"effective": {}
}
Get invitation question rules
GET /events/{slug}/invitations/{id}/settings/fields
Read selected, inherited, and effective appearances for active person fields. ticket_types names the ticket types offered here that ask each question. A ticket type's question is asked on the RSVP unless a scope turns it off, and effective_* includes it. Only organizer-owned records in the named event are accessible.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
idinteger required
Response fields
fieldsarray of objectfields[].field_idintegerfields[].ticket_typesarray of object- The ticket types offered at this scope that ask this question, so an RSVP for one of them asks it unless this scope turns it off. Empty for a question only the invitation asks.
fields[].ticket_types[].idintegerfields[].ticket_types[].namestringfields[].ticket_types[].requiredbooleanfields[].keystringfields[].labelstringfields[].kindstring
Responses
200OK401No 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/invitations/1/settings/fields \
-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/invitations/1/settings/fields", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"fields": [
{
"field_id": 1,
"ticket_types": [
{
"id": 1,
"name": "Ada Lovelace",
"required": true
}
],
"key": "example",
"label": "example",
"kind": "ticket",
"primary": "optional",
"guest": "optional",
"inherited_primary": "optional",
"inherited_guest": "optional",
"effective_primary": "optional",
"effective_guest": "optional"
}
]
}
Update invitation question rules
PATCH /events/{slug}/invitations/{id}/settings/fields
Atomically update supplied field and audience cells. Omitted cells stay unchanged. optional is refused for a question a ticket type requires. default removes the local override. off without an inherited rule removes a redundant override. Empty fields is a no-op. Null, duplicate cells, and inactive fields are refused.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Request body
Response fields
fieldsarray of objectfields[].field_idintegerfields[].ticket_typesarray of object- The ticket types offered at this scope that ask this question, so an RSVP for one of them asks it unless this scope turns it off. Empty for a question only the invitation asks.
fields[].ticket_types[].idintegerfields[].ticket_types[].namestringfields[].ticket_types[].requiredbooleanfields[].keystringfields[].labelstringfields[].kindstring
Responses
200OK401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Each cell needs field_id, audience (primary or guest), and appearance. Each field and audience pair must be unique.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/invitations/1/settings/fields \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"fields":[{"field_id":1,"audience":"primary","appearance":"optional"}]}'
// 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/invitations/1/settings/fields", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"fields": [
{
"field_id": 1,
"audience": "primary",
"appearance": "optional"
}
]
}),
});
const data = await res.json();
{
"fields": [
{
"field_id": 1,
"ticket_types": [
{
"id": 1,
"name": "Ada Lovelace",
"required": true
}
],
"key": "example",
"label": "example",
"kind": "ticket",
"primary": "optional",
"guest": "optional",
"inherited_primary": "optional",
"inherited_guest": "optional",
"effective_primary": "optional",
"effective_guest": "optional"
}
]
}
Audiences
List audiences
GET /events/{slug}/audiences
An audience is a named public page of the event showing some of its ticket types — a booking page for a group, at /<event>/<slug>, with its own per-order limit, promo-code box and quote button. Audience writes keep no history rows, as the admin's don't. In the event's own order.
Requires catalog.view
Path parameters
slugstring required- The event's slug.
Response fields
audiencesarray of Audienceaudiences[].namestringaudiences[].show_bank_transferboolean- Whether this page's buyers are offered bank transfer, when the event offers it. Defaults to on when an audience is created.
audiences[].show_express_checkoutboolean- Whether this page's buyers are offered express checkout: Apple Pay, Google Pay, Link and PayPal. Card payment is offered either way. Defaults to on when an audience is created. The event's own
express_checkout_enabledoff wins over this. audiences[].organizations_onlyboolean- Whether every booking on this page is made out to an organization: the checkout asks for the organization's name, always, with no checkbox, even where the event does not ask about companies. Defaults to off.
audiences[].ask_vatboolean- Whether the checkout's organization details ask for a VAT number. Off means no box, and a VAT number posted anyway is not stored. Defaults to on.
audiences[].countriesarray of string- The countries this page's buyers can be in, as ISO 3166-1 alpha-2 codes; empty for any. The checkout offers only these, and with exactly one it does not ask: that country is stored on every order.
audiences[].created_atstring (RFC 3339) or null
Responses
200Audiences.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/audiences \
-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/audiences", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"audiences": [
{
"id": 6,
"slug": "press",
"url": "https://acct.go.tito.io/annual/press",
"name": "Press",
"description": "For accredited press.",
"max_per_order": 2,
"show_promo_field": true,
"show_bank_transfer": true,
"show_express_checkout": true,
"show_quote_button": false,
"ref_prefix": "PR",
"organizations_only": false,
"ask_vat": true,
"countries": [
"IE",
"GB"
],
"ticket_type_ids": [
8
],
"closed": false,
"created_at": "2026-09-07T21:33:00Z"
}
]
}
Create an audience
POST /events/{slug}/audiences
A new page. A blank slug derives from the name; a taken one is 409 slug_taken, a reserved or malformed one 422 invalid_slug. ticket_type_ids sets which types the page shows.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
Request body
namestringslugstring- Create only: lowercase letters, digits and hyphens, up to 40; derived from the name when blank.
descriptionstringshow_promo_fieldbooleanshow_bank_transferbooleanshow_express_checkoutbooleanshow_quote_buttonbooleanorganizations_onlybooleancountriesarray of string- ISO 3166-1 alpha-2 codes, any case; replaces the whole list when sent, and [] allows any country. An unknown code is refused with
422 invalid_body.
Response fields
namestringshow_bank_transferboolean- Whether this page's buyers are offered bank transfer, when the event offers it. Defaults to on when an audience is created.
show_express_checkoutboolean- Whether this page's buyers are offered express checkout: Apple Pay, Google Pay, Link and PayPal. Card payment is offered either way. Defaults to on when an audience is created. The event's own
express_checkout_enabledoff wins over this. organizations_onlyboolean- Whether every booking on this page is made out to an organization: the checkout asks for the organization's name, always, with no checkbox, even where the event does not ask about companies. Defaults to off.
ask_vatboolean- Whether the checkout's organization details ask for a VAT number. Off means no box, and a VAT number posted anyway is not stored. Defaults to on.
countriesarray of string- The countries this page's buyers can be in, as ISO 3166-1 alpha-2 codes; empty for any. The checkout offers only these, and with exactly one it does not ask: that country is stored on every order.
created_atstring (RFC 3339) or null
Responses
201The new audience.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409An audience already has that slug on this event (slug_taken).422The body was unreadable or carried an unknown field, the name was blank,max_per_orderis outside 0–500,ref_prefixis not one, or a ticket type is not this event's (invalid_body); a reserved or malformed slug (invalid_slug).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/audiences \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Press"}'
// 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/audiences", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Press"
}),
});
const data = await res.json();
{
"id": 6,
"slug": "press",
"url": "https://acct.go.tito.io/annual/press",
"name": "Press",
"description": "For accredited press.",
"max_per_order": 2,
"show_promo_field": true,
"show_bank_transfer": true,
"show_express_checkout": true,
"show_quote_button": false,
"ref_prefix": "PR",
"organizations_only": false,
"ask_vat": true,
"countries": [
"IE",
"GB"
],
"ticket_type_ids": [
8
],
"closed": false,
"created_at": "2026-09-07T21:33:00Z"
}
Delete an audience
DELETE /events/{slug}/audiences/{id}
Removes the page outright. One that still shows ticket types is 409 in_use: send ticket_type_ids: [] first, as the admin makes you take them off.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The audience's id, as
GET /events/{slug}/audienceslists it.
Response fields
Responses
200Deleted.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The audience still shows ticket types (in_use).500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/events/annual-conf/audiences/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/audiences/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 6,
"deleted": true
}
Get an audience
GET /events/{slug}/audiences/{id}
One audience, with the types it shows.
Requires catalog.view
Path parameters
slugstring required- The event's slug.
idinteger required- The audience's id, as
GET /events/{slug}/audienceslists it.
Response fields
namestringshow_bank_transferboolean- Whether this page's buyers are offered bank transfer, when the event offers it. Defaults to on when an audience is created.
show_express_checkoutboolean- Whether this page's buyers are offered express checkout: Apple Pay, Google Pay, Link and PayPal. Card payment is offered either way. Defaults to on when an audience is created. The event's own
express_checkout_enabledoff wins over this. organizations_onlyboolean- Whether every booking on this page is made out to an organization: the checkout asks for the organization's name, always, with no checkbox, even where the event does not ask about companies. Defaults to off.
ask_vatboolean- Whether the checkout's organization details ask for a VAT number. Off means no box, and a VAT number posted anyway is not stored. Defaults to on.
countriesarray of string- The countries this page's buyers can be in, as ISO 3166-1 alpha-2 codes; empty for any. The checkout offers only these, and with exactly one it does not ask: that country is stored on every order.
created_atstring (RFC 3339) or null
Responses
200The audience.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/audiences/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/audiences/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 6,
"slug": "press",
"url": "https://acct.go.tito.io/annual/press",
"name": "Press",
"description": "For accredited press.",
"max_per_order": 2,
"show_promo_field": true,
"show_bank_transfer": true,
"show_express_checkout": true,
"show_quote_button": false,
"ref_prefix": "PR",
"organizations_only": false,
"ask_vat": true,
"countries": [
"IE",
"GB"
],
"ticket_type_ids": [
8
],
"closed": false,
"created_at": "2026-09-07T21:33:00Z"
}
Update an audience
PATCH /events/{slug}/audiences/{id}
The edit form's fields; absent ones are unchanged, an explicit null and an unknown field are refused. ticket_type_ids, when sent, replaces which types the page shows. The slug is the page's address and cannot change.
Requires catalog.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The audience's id, as
GET /events/{slug}/audienceslists it.
Request body
namestringslugstring- Create only: lowercase letters, digits and hyphens, up to 40; derived from the name when blank.
descriptionstringshow_promo_fieldbooleanshow_bank_transferbooleanshow_express_checkoutbooleanshow_quote_buttonbooleanorganizations_onlybooleancountriesarray of string- ISO 3166-1 alpha-2 codes, any case; replaces the whole list when sent, and [] allows any country. An unknown code is refused with
422 invalid_body.
Response fields
namestringshow_bank_transferboolean- Whether this page's buyers are offered bank transfer, when the event offers it. Defaults to on when an audience is created.
show_express_checkoutboolean- Whether this page's buyers are offered express checkout: Apple Pay, Google Pay, Link and PayPal. Card payment is offered either way. Defaults to on when an audience is created. The event's own
express_checkout_enabledoff wins over this. organizations_onlyboolean- Whether every booking on this page is made out to an organization: the checkout asks for the organization's name, always, with no checkbox, even where the event does not ask about companies. Defaults to off.
ask_vatboolean- Whether the checkout's organization details ask for a VAT number. Off means no box, and a VAT number posted anyway is not stored. Defaults to on.
countriesarray of string- The countries this page's buyers can be in, as ISO 3166-1 alpha-2 codes; empty for any. The checkout offers only these, and with exactly one it does not ask: that country is stored on every order.
created_atstring (RFC 3339) or null
Responses
200The audience as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422As for create, orslugsent (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/audiences/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Press"}'
// 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/audiences/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Press"
}),
});
const data = await res.json();
{
"id": 6,
"slug": "press",
"url": "https://acct.go.tito.io/annual/press",
"name": "Press",
"description": "For accredited press.",
"max_per_order": 2,
"show_promo_field": true,
"show_bank_transfer": true,
"show_express_checkout": true,
"show_quote_button": false,
"ref_prefix": "PR",
"organizations_only": false,
"ask_vat": true,
"countries": [
"IE",
"GB"
],
"ticket_type_ids": [
8
],
"closed": false,
"created_at": "2026-09-07T21:33:00Z"
}
Portals
List portals
GET /events/{slug}/portals
A portal is a sponsor's or partner's own page with an allocation of the event's tickets to hand out — issued as invitations from a hidden list of its own. Its url is the holder's dashboard, a bearer link; share_url is the public registration address, null while sharing is off or the link has nothing to give. Every write is recorded in the event's activity against the key. The access list (who may sign in on a locked portal) is never reported, only the switch. Live portals first, newest first.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
Response fields
portalsarray of Portalportals[].slugstring- The readable share-address segment, with its random tail. A PATCH with different words moves it to a new tail, and regenerate gives it a new tail.
portals[].namestringportals[].per_typeboolean- Whether the allocation is a set number of EACH type (the sum of
ticket_types[].allocation) rather than a pool. portals[].ticket_types[].allocationinteger- This type's own number on a per-type portal; 0 on a pooled one.
portals[].package_due_datestring or null- The day the fee is due, on the event's clock; null for "before they can hand anything out".
portals[].package_tax_rate_idinteger- The tax rate the fee is charged at, from the account's rates; 0 for no VAT.
portals[].package_price_includes_taxboolean- Whether package_cents already includes the VAT, or the VAT is added on top. Always false with no rate.
portals[].capabilitiesarray of string- The What it does set switched on; empty is plain tickets. Sections: offerings, details, documents, downloads, guests. The holder's own ticket controls: change_type, buy_more, team. names_only means the holder gives a name and an email when assigning a ticket, and the attendee answers the event's questions.
portals[].access_lockedboolean- Whether the holder must sign in with an address the access list admits. The list itself is not reported.
portals[].give_out_untilstring (RFC 3339) or null- The portal's cut-off: after this moment the holder can no longer send links, assign tickets, upload a list or register people on the share link. Tickets already given out, and links already sent, keep working. null for no cut-off.
portals[].archivedbooleanportals[].revokedbooleanportals[].created_atstring (RFC 3339) or null
Responses
200Portals.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/portals \
-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/portals", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"portals": [
{
"id": 3,
"ref": "P7K2QM",
"slug": "amgen-k8wa6",
"name": "Amgen",
"url": "https://acct.go.tito.io/annual/portals/1qR20rFnPEV70bqeGNLJZg",
"share_url": null,
"shareable": false,
"share_ticket_type_ids": [
12,
14
],
"contact_email": "sponsor@example.com",
"currency": "eur",
"allocation": 100,
"per_type": false,
"ticket_types": [
{
"allocation": 0,
"id": 8
}
],
"issued": 12,
"pending": 3,
"remaining": 85,
"ref_prefix": "SPN",
"package": "Gold stand",
"package_cents": 250000,
"package_detail": "Two passes and a stand",
"package_due_date": "2030-03-01",
"package_tax_rate_id": 0,
"package_price_includes_tax": false,
"capabilities": [
"offerings"
],
"access_locked": false,
"give_out_until": null,
"archived": false,
"revoked": false,
"sent_at": null,
"created_at": "2026-09-07T22:00:00Z"
}
]
}
Create a portal
POST /events/{slug}/portals
A new portal, pooled (allocation plus ticket_type_ids), per type (ticket_types, each with its own number, the total being their sum), or with no tickets at all when neither is sent (allocation 0, to share files or ask for details, or to set up ahead of the allocation; tickets are added later from the admin's tickets checklist). The slug derives from the name when blank and always takes a random tail, as every share address does; currency is chosen here and frozen. No email goes out: POST …/{id}/send does that.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
Request body
namestringslugstring- The shareable link's readable words: lowercase letters, digits and hyphens, up to 60; derived from the name when blank. A random tail is always appended, so the answer's
slugends in five more characters. On a PATCH, different words move the address and the old one stops working. ticket_typesarray of PortalType- Create only: a per-type portal's scope, each type with its own number (1–1000000).
ticket_types[].allocationinteger required- This type's own number on a per-type portal; 0 on a pooled one.
packagestringpackage_detailstringpackage_tax_rate_idinteger- A tax rate's id, or 0 for no VAT. Ignored (stored as 0) when the portal charges no fee. Refused with package_billed while a bill stands.
package_price_includes_taxboolean- Whether package_cents already includes the VAT; false adds it on top. Refused with package_billed while a bill stands.
access_lockedboolean- Switch the sign-in lock; needs at least one access_list entry. A portal whose team capability is on stays locked, and unlocking it is refused with team_locked.
access_listarray of string- Addresses and domains the lock admits; replaces the whole list when sent.
give_out_untilstring- The cut-off as an RFC 3339 moment, or "" for none. Refused with invalid_dates when it will not read.
Response fields
slugstring- The readable share-address segment, with its random tail. A PATCH with different words moves it to a new tail, and regenerate gives it a new tail.
namestringper_typeboolean- Whether the allocation is a set number of EACH type (the sum of
ticket_types[].allocation) rather than a pool. package_due_datestring or null- The day the fee is due, on the event's clock; null for "before they can hand anything out".
package_tax_rate_idinteger- The tax rate the fee is charged at, from the account's rates; 0 for no VAT.
package_price_includes_taxboolean- Whether package_cents already includes the VAT, or the VAT is added on top. Always false with no rate.
capabilitiesarray of string- The What it does set switched on; empty is plain tickets. Sections: offerings, details, documents, downloads, guests. The holder's own ticket controls: change_type, buy_more, team. names_only means the holder gives a name and an email when assigning a ticket, and the attendee answers the event's questions.
access_lockedboolean- Whether the holder must sign in with an address the access list admits. The list itself is not reported.
give_out_untilstring (RFC 3339) or null- The portal's cut-off: after this moment the holder can no longer send links, assign tickets, upload a list or register people on the share link. Tickets already given out, and links already sent, keep working. null for no cut-off.
archivedbooleanrevokedbooleancreated_atstring (RFC 3339) or null
Responses
201The new portal.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body was unreadable or carried an unknown field, the name was blank, both scope shapes were sent, an allocation was sent without ticket types, an allocation is outside 1–1000000, a ticket type is not this event's or is listed twice, an email, prefix, fee, due date or access entry is malformed, the portal is locked with an empty access list, or the currency is not one the event sells in (invalid_body); a malformed slug (invalid_slug).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/portals \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Amgen"}'
// 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/portals", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Amgen"
}),
});
const data = await res.json();
{
"id": 3,
"ref": "P7K2QM",
"slug": "amgen-k8wa6",
"name": "Amgen",
"url": "https://acct.go.tito.io/annual/portals/1qR20rFnPEV70bqeGNLJZg",
"share_url": null,
"shareable": false,
"share_ticket_type_ids": [
12,
14
],
"contact_email": "sponsor@example.com",
"currency": "eur",
"allocation": 100,
"per_type": false,
"ticket_types": [
{
"allocation": 0,
"id": 8
}
],
"issued": 12,
"pending": 3,
"remaining": 85,
"ref_prefix": "SPN",
"package": "Gold stand",
"package_cents": 250000,
"package_detail": "Two passes and a stand",
"package_due_date": "2030-03-01",
"package_tax_rate_id": 0,
"package_price_includes_tax": false,
"capabilities": [
"offerings"
],
"access_locked": false,
"give_out_until": null,
"archived": false,
"revoked": false,
"sent_at": null,
"created_at": "2026-09-07T22:00:00Z"
}
Delete a portal
DELETE /events/{slug}/portals/{id}
Only a portal nobody has used: no invitation issued from it, no order placed through it, no answer, upload or guest list from its holder, nothing done by its holder, and no package bill raised. Its set-up goes with it — the hidden invitation list and its ticket types, what it sells, its downloads and what it asks. Having been sent is not use; the link stops working. A used portal is refused with portal_in_use: archive or revoke it instead.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The portal's id, as
GET /events/{slug}/portalslists it.
Response fields
Responses
200Deleted.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The portal has been used (portal_in_use).500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/events/annual-conf/portals/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/portals/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 4,
"deleted": true
}
Get a portal
GET /events/{slug}/portals/{id}
One portal, with its scope and drawdown.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
idinteger required- The portal's id, as
GET /events/{slug}/portalslists it.
Response fields
slugstring- The readable share-address segment, with its random tail. A PATCH with different words moves it to a new tail, and regenerate gives it a new tail.
namestringper_typeboolean- Whether the allocation is a set number of EACH type (the sum of
ticket_types[].allocation) rather than a pool. package_due_datestring or null- The day the fee is due, on the event's clock; null for "before they can hand anything out".
package_tax_rate_idinteger- The tax rate the fee is charged at, from the account's rates; 0 for no VAT.
package_price_includes_taxboolean- Whether package_cents already includes the VAT, or the VAT is added on top. Always false with no rate.
capabilitiesarray of string- The What it does set switched on; empty is plain tickets. Sections: offerings, details, documents, downloads, guests. The holder's own ticket controls: change_type, buy_more, team. names_only means the holder gives a name and an email when assigning a ticket, and the attendee answers the event's questions.
access_lockedboolean- Whether the holder must sign in with an address the access list admits. The list itself is not reported.
give_out_untilstring (RFC 3339) or null- The portal's cut-off: after this moment the holder can no longer send links, assign tickets, upload a list or register people on the share link. Tickets already given out, and links already sent, keep working. null for no cut-off.
archivedbooleanrevokedbooleancreated_atstring (RFC 3339) or null
Responses
200The portal.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/portals/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/portals/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"ref": "P7K2QM",
"slug": "amgen-k8wa6",
"name": "Amgen",
"url": "https://acct.go.tito.io/annual/portals/1qR20rFnPEV70bqeGNLJZg",
"share_url": null,
"shareable": false,
"share_ticket_type_ids": [
12,
14
],
"contact_email": "sponsor@example.com",
"currency": "eur",
"allocation": 100,
"per_type": false,
"ticket_types": [
{
"allocation": 0,
"id": 8
}
],
"issued": 12,
"pending": 3,
"remaining": 85,
"ref_prefix": "SPN",
"package": "Gold stand",
"package_cents": 250000,
"package_detail": "Two passes and a stand",
"package_due_date": "2030-03-01",
"package_tax_rate_id": 0,
"package_price_includes_tax": false,
"capabilities": [
"offerings"
],
"access_locked": false,
"give_out_until": null,
"archived": false,
"revoked": false,
"sent_at": null,
"created_at": "2026-09-07T22:00:00Z"
}
Update a portal
PATCH /events/{slug}/portals/{id}
The Basics form's fields; absent ones are unchanged, an explicit null and an unknown field are refused. currency and the ticket types are fixed at creation (the scope is the admin's tickets checklist). A slug gives the shareable link new words: a different one gets a fresh random tail and the old address stops working straight away. The same words, or the current slug with its tail, change nothing, and a blank one is derived from the name. It is read with sharing off too, and sets the words the link opens at when it is switched on. A per-type portal's allocation is the sum of its numbers and cannot be sent, and a portal with no tickets takes its first allocation with its ticket types in the admin, so sending one is 422 no_tickets.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The portal's id, as
GET /events/{slug}/portalslists it.
Request body
namestringslugstring- The shareable link's readable words: lowercase letters, digits and hyphens, up to 60; derived from the name when blank. A random tail is always appended, so the answer's
slugends in five more characters. On a PATCH, different words move the address and the old one stops working. ticket_typesarray of PortalType- Create only: a per-type portal's scope, each type with its own number (1–1000000).
ticket_types[].allocationinteger required- This type's own number on a per-type portal; 0 on a pooled one.
packagestringpackage_detailstringpackage_tax_rate_idinteger- A tax rate's id, or 0 for no VAT. Ignored (stored as 0) when the portal charges no fee. Refused with package_billed while a bill stands.
package_price_includes_taxboolean- Whether package_cents already includes the VAT; false adds it on top. Refused with package_billed while a bill stands.
access_lockedboolean- Switch the sign-in lock; needs at least one access_list entry. A portal whose team capability is on stays locked, and unlocking it is refused with team_locked.
access_listarray of string- Addresses and domains the lock admits; replaces the whole list when sent.
give_out_untilstring- The cut-off as an RFC 3339 moment, or "" for none. Refused with invalid_dates when it will not read.
Response fields
slugstring- The readable share-address segment, with its random tail. A PATCH with different words moves it to a new tail, and regenerate gives it a new tail.
namestringper_typeboolean- Whether the allocation is a set number of EACH type (the sum of
ticket_types[].allocation) rather than a pool. package_due_datestring or null- The day the fee is due, on the event's clock; null for "before they can hand anything out".
package_tax_rate_idinteger- The tax rate the fee is charged at, from the account's rates; 0 for no VAT.
package_price_includes_taxboolean- Whether package_cents already includes the VAT, or the VAT is added on top. Always false with no rate.
capabilitiesarray of string- The What it does set switched on; empty is plain tickets. Sections: offerings, details, documents, downloads, guests. The holder's own ticket controls: change_type, buy_more, team. names_only means the holder gives a name and an email when assigning a ticket, and the attendee answers the event's questions.
access_lockedboolean- Whether the holder must sign in with an address the access list admits. The list itself is not reported.
give_out_untilstring (RFC 3339) or null- The portal's cut-off: after this moment the holder can no longer send links, assign tickets, upload a list or register people on the share link. Tickets already given out, and links already sent, keep working. null for no cut-off.
archivedbooleanrevokedbooleancreated_atstring (RFC 3339) or null
Responses
200The portal as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The allocation is below what is already issued or pending (allocation_used), or the fee changed while a bill for it stands (package_billed).422As for create, or a fixed field sent (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/portals/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Amgen"}'
// 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/portals/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Amgen"
}),
});
const data = await res.json();
{
"id": 3,
"ref": "P7K2QM",
"slug": "amgen-k8wa6",
"name": "Amgen",
"url": "https://acct.go.tito.io/annual/portals/1qR20rFnPEV70bqeGNLJZg",
"share_url": null,
"shareable": false,
"share_ticket_type_ids": [
12,
14
],
"contact_email": "sponsor@example.com",
"currency": "eur",
"allocation": 100,
"per_type": false,
"ticket_types": [
{
"allocation": 0,
"id": 8
}
],
"issued": 12,
"pending": 3,
"remaining": 85,
"ref_prefix": "SPN",
"package": "Gold stand",
"package_cents": 250000,
"package_detail": "Two passes and a stand",
"package_due_date": "2030-03-01",
"package_tax_rate_id": 0,
"package_price_includes_tax": false,
"capabilities": [
"offerings"
],
"access_locked": false,
"give_out_until": null,
"archived": false,
"revoked": false,
"sent_at": null,
"created_at": "2026-09-07T22:00:00Z"
}
Archive a portal
POST /events/{slug}/portals/{id}/archive
Puts the portal away; its links stop working. Reversible with unarchive.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The portal's id, as
GET /events/{slug}/portalslists it.
Response fields
slugstring- The readable share-address segment, with its random tail. A PATCH with different words moves it to a new tail, and regenerate gives it a new tail.
namestringper_typeboolean- Whether the allocation is a set number of EACH type (the sum of
ticket_types[].allocation) rather than a pool. package_due_datestring or null- The day the fee is due, on the event's clock; null for "before they can hand anything out".
package_tax_rate_idinteger- The tax rate the fee is charged at, from the account's rates; 0 for no VAT.
package_price_includes_taxboolean- Whether package_cents already includes the VAT, or the VAT is added on top. Always false with no rate.
capabilitiesarray of string- The What it does set switched on; empty is plain tickets. Sections: offerings, details, documents, downloads, guests. The holder's own ticket controls: change_type, buy_more, team. names_only means the holder gives a name and an email when assigning a ticket, and the attendee answers the event's questions.
access_lockedboolean- Whether the holder must sign in with an address the access list admits. The list itself is not reported.
give_out_untilstring (RFC 3339) or null- The portal's cut-off: after this moment the holder can no longer send links, assign tickets, upload a list or register people on the share link. Tickets already given out, and links already sent, keep working. null for no cut-off.
archivedbooleanrevokedbooleancreated_atstring (RFC 3339) or null
Responses
200The portal as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/portals/1/archive \
-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/portals/1/archive", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"ref": "P7K2QM",
"slug": "amgen-k8wa6",
"name": "Amgen",
"url": "https://acct.go.tito.io/annual/portals/1qR20rFnPEV70bqeGNLJZg",
"share_url": null,
"shareable": false,
"share_ticket_type_ids": [
12,
14
],
"contact_email": "sponsor@example.com",
"currency": "eur",
"allocation": 100,
"per_type": false,
"ticket_types": [
{
"allocation": 0,
"id": 8
}
],
"issued": 12,
"pending": 3,
"remaining": 85,
"ref_prefix": "SPN",
"package": "Gold stand",
"package_cents": 250000,
"package_detail": "Two passes and a stand",
"package_due_date": "2030-03-01",
"package_tax_rate_id": 0,
"package_price_includes_tax": false,
"capabilities": [
"offerings"
],
"access_locked": false,
"give_out_until": null,
"archived": false,
"revoked": false,
"sent_at": null,
"created_at": "2026-09-07T22:00:00Z"
}
Regenerate a portal's links
POST /events/{slug}/portals/{id}/regenerate
Mints a fresh holder link (the old url stops working), a fresh share link when sharing is on, a fresh random tail on the readable share address (slug, the same words), and clears any revocation: the leaked-link escape hatch.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The portal's id, as
GET /events/{slug}/portalslists it.
Response fields
slugstring- The readable share-address segment, with its random tail. A PATCH with different words moves it to a new tail, and regenerate gives it a new tail.
namestringper_typeboolean- Whether the allocation is a set number of EACH type (the sum of
ticket_types[].allocation) rather than a pool. package_due_datestring or null- The day the fee is due, on the event's clock; null for "before they can hand anything out".
package_tax_rate_idinteger- The tax rate the fee is charged at, from the account's rates; 0 for no VAT.
package_price_includes_taxboolean- Whether package_cents already includes the VAT, or the VAT is added on top. Always false with no rate.
capabilitiesarray of string- The What it does set switched on; empty is plain tickets. Sections: offerings, details, documents, downloads, guests. The holder's own ticket controls: change_type, buy_more, team. names_only means the holder gives a name and an email when assigning a ticket, and the attendee answers the event's questions.
access_lockedboolean- Whether the holder must sign in with an address the access list admits. The list itself is not reported.
give_out_untilstring (RFC 3339) or null- The portal's cut-off: after this moment the holder can no longer send links, assign tickets, upload a list or register people on the share link. Tickets already given out, and links already sent, keep working. null for no cut-off.
archivedbooleanrevokedbooleancreated_atstring (RFC 3339) or null
Responses
200The portal as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/portals/1/regenerate \
-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/portals/1/regenerate", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"ref": "P7K2QM",
"slug": "amgen-k8wa6",
"name": "Amgen",
"url": "https://acct.go.tito.io/annual/portals/1qR20rFnPEV70bqeGNLJZg",
"share_url": null,
"shareable": false,
"share_ticket_type_ids": [
12,
14
],
"contact_email": "sponsor@example.com",
"currency": "eur",
"allocation": 100,
"per_type": false,
"ticket_types": [
{
"allocation": 0,
"id": 8
}
],
"issued": 12,
"pending": 3,
"remaining": 85,
"ref_prefix": "SPN",
"package": "Gold stand",
"package_cents": 250000,
"package_detail": "Two passes and a stand",
"package_due_date": "2030-03-01",
"package_tax_rate_id": 0,
"package_price_includes_tax": false,
"capabilities": [
"offerings"
],
"access_locked": false,
"give_out_until": null,
"archived": false,
"revoked": false,
"sent_at": null,
"created_at": "2026-09-07T22:00:00Z"
}
Reinstate a portal's links
POST /events/{slug}/portals/{id}/reinstate
Switches a revoked portal's links back on.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The portal's id, as
GET /events/{slug}/portalslists it.
Response fields
slugstring- The readable share-address segment, with its random tail. A PATCH with different words moves it to a new tail, and regenerate gives it a new tail.
namestringper_typeboolean- Whether the allocation is a set number of EACH type (the sum of
ticket_types[].allocation) rather than a pool. package_due_datestring or null- The day the fee is due, on the event's clock; null for "before they can hand anything out".
package_tax_rate_idinteger- The tax rate the fee is charged at, from the account's rates; 0 for no VAT.
package_price_includes_taxboolean- Whether package_cents already includes the VAT, or the VAT is added on top. Always false with no rate.
capabilitiesarray of string- The What it does set switched on; empty is plain tickets. Sections: offerings, details, documents, downloads, guests. The holder's own ticket controls: change_type, buy_more, team. names_only means the holder gives a name and an email when assigning a ticket, and the attendee answers the event's questions.
access_lockedboolean- Whether the holder must sign in with an address the access list admits. The list itself is not reported.
give_out_untilstring (RFC 3339) or null- The portal's cut-off: after this moment the holder can no longer send links, assign tickets, upload a list or register people on the share link. Tickets already given out, and links already sent, keep working. null for no cut-off.
archivedbooleanrevokedbooleancreated_atstring (RFC 3339) or null
Responses
200The portal as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/portals/1/reinstate \
-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/portals/1/reinstate", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"ref": "P7K2QM",
"slug": "amgen-k8wa6",
"name": "Amgen",
"url": "https://acct.go.tito.io/annual/portals/1qR20rFnPEV70bqeGNLJZg",
"share_url": null,
"shareable": false,
"share_ticket_type_ids": [
12,
14
],
"contact_email": "sponsor@example.com",
"currency": "eur",
"allocation": 100,
"per_type": false,
"ticket_types": [
{
"allocation": 0,
"id": 8
}
],
"issued": 12,
"pending": 3,
"remaining": 85,
"ref_prefix": "SPN",
"package": "Gold stand",
"package_cents": 250000,
"package_detail": "Two passes and a stand",
"package_due_date": "2030-03-01",
"package_tax_rate_id": 0,
"package_price_includes_tax": false,
"capabilities": [
"offerings"
],
"access_locked": false,
"give_out_until": null,
"archived": false,
"revoked": false,
"sent_at": null,
"created_at": "2026-09-07T22:00:00Z"
}
Revoke a portal's links
POST /events/{slug}/portals/{id}/revoke
Switches the holder's link (and the share link) off without archiving. Reversible with reinstate, or by regenerate.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The portal's id, as
GET /events/{slug}/portalslists it.
Response fields
slugstring- The readable share-address segment, with its random tail. A PATCH with different words moves it to a new tail, and regenerate gives it a new tail.
namestringper_typeboolean- Whether the allocation is a set number of EACH type (the sum of
ticket_types[].allocation) rather than a pool. package_due_datestring or null- The day the fee is due, on the event's clock; null for "before they can hand anything out".
package_tax_rate_idinteger- The tax rate the fee is charged at, from the account's rates; 0 for no VAT.
package_price_includes_taxboolean- Whether package_cents already includes the VAT, or the VAT is added on top. Always false with no rate.
capabilitiesarray of string- The What it does set switched on; empty is plain tickets. Sections: offerings, details, documents, downloads, guests. The holder's own ticket controls: change_type, buy_more, team. names_only means the holder gives a name and an email when assigning a ticket, and the attendee answers the event's questions.
access_lockedboolean- Whether the holder must sign in with an address the access list admits. The list itself is not reported.
give_out_untilstring (RFC 3339) or null- The portal's cut-off: after this moment the holder can no longer send links, assign tickets, upload a list or register people on the share link. Tickets already given out, and links already sent, keep working. null for no cut-off.
archivedbooleanrevokedbooleancreated_atstring (RFC 3339) or null
Responses
200The portal as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/portals/1/revoke \
-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/portals/1/revoke", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"ref": "P7K2QM",
"slug": "amgen-k8wa6",
"name": "Amgen",
"url": "https://acct.go.tito.io/annual/portals/1qR20rFnPEV70bqeGNLJZg",
"share_url": null,
"shareable": false,
"share_ticket_type_ids": [
12,
14
],
"contact_email": "sponsor@example.com",
"currency": "eur",
"allocation": 100,
"per_type": false,
"ticket_types": [
{
"allocation": 0,
"id": 8
}
],
"issued": 12,
"pending": 3,
"remaining": 85,
"ref_prefix": "SPN",
"package": "Gold stand",
"package_cents": 250000,
"package_detail": "Two passes and a stand",
"package_due_date": "2030-03-01",
"package_tax_rate_id": 0,
"package_price_includes_tax": false,
"capabilities": [
"offerings"
],
"access_locked": false,
"give_out_until": null,
"archived": false,
"revoked": false,
"sent_at": null,
"created_at": "2026-09-07T22:00:00Z"
}
Email a portal's link
POST /events/{slug}/portals/{id}/send
Emails the holder their dashboard link — to to, or the contact address — with the account's stock wording when subject or body is blank. Answers the portal with delivery: sent, logged (no mail transport) or failed.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The portal's id, as
GET /events/{slug}/portalslists it.
Request body
Response fields
portal.slugstring- The readable share-address segment, with its random tail. A PATCH with different words moves it to a new tail, and regenerate gives it a new tail.
portal.namestringportal.per_typeboolean- Whether the allocation is a set number of EACH type (the sum of
ticket_types[].allocation) rather than a pool. portal.ticket_types[].allocationinteger- This type's own number on a per-type portal; 0 on a pooled one.
portal.package_due_datestring or null- The day the fee is due, on the event's clock; null for "before they can hand anything out".
portal.package_tax_rate_idinteger- The tax rate the fee is charged at, from the account's rates; 0 for no VAT.
portal.package_price_includes_taxboolean- Whether package_cents already includes the VAT, or the VAT is added on top. Always false with no rate.
portal.capabilitiesarray of string- The What it does set switched on; empty is plain tickets. Sections: offerings, details, documents, downloads, guests. The holder's own ticket controls: change_type, buy_more, team. names_only means the holder gives a name and an email when assigning a ticket, and the attendee answers the event's questions.
portal.access_lockedboolean- Whether the holder must sign in with an address the access list admits. The list itself is not reported.
portal.give_out_untilstring (RFC 3339) or null- The portal's cut-off: after this moment the holder can no longer send links, assign tickets, upload a list or register people on the share link. Tickets already given out, and links already sent, keep working. null for no cut-off.
portal.archivedbooleanportal.revokedbooleanportal.created_atstring (RFC 3339) or null
Responses
200Sent.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422No address to send to, or a malformed one (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/portals/1/send \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"to":"sponsor@example.com"}'
// 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/portals/1/send", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"to": "sponsor@example.com"
}),
});
const data = await res.json();
{
"portal": {
"id": 3,
"ref": "P7K2QM",
"slug": "amgen-k8wa6",
"name": "Amgen",
"url": "https://acct.go.tito.io/annual/portals/1qR20rFnPEV70bqeGNLJZg",
"share_url": null,
"shareable": false,
"share_ticket_type_ids": [
12,
14
],
"contact_email": "sponsor@example.com",
"currency": "eur",
"allocation": 100,
"per_type": false,
"ticket_types": [
{
"allocation": 0,
"id": 8
}
],
"issued": 12,
"pending": 3,
"remaining": 85,
"ref_prefix": "SPN",
"package": "Gold stand",
"package_cents": 250000,
"package_detail": "Two passes and a stand",
"package_due_date": "2030-03-01",
"package_tax_rate_id": 0,
"package_price_includes_tax": false,
"capabilities": [
"offerings"
],
"access_locked": false,
"give_out_until": null,
"archived": false,
"revoked": false,
"sent_at": null,
"created_at": "2026-09-07T22:00:00Z"
},
"delivery": "sent"
}
Unarchive a portal
POST /events/{slug}/portals/{id}/unarchive
Brings an archived portal back.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The portal's id, as
GET /events/{slug}/portalslists it.
Response fields
slugstring- The readable share-address segment, with its random tail. A PATCH with different words moves it to a new tail, and regenerate gives it a new tail.
namestringper_typeboolean- Whether the allocation is a set number of EACH type (the sum of
ticket_types[].allocation) rather than a pool. package_due_datestring or null- The day the fee is due, on the event's clock; null for "before they can hand anything out".
package_tax_rate_idinteger- The tax rate the fee is charged at, from the account's rates; 0 for no VAT.
package_price_includes_taxboolean- Whether package_cents already includes the VAT, or the VAT is added on top. Always false with no rate.
capabilitiesarray of string- The What it does set switched on; empty is plain tickets. Sections: offerings, details, documents, downloads, guests. The holder's own ticket controls: change_type, buy_more, team. names_only means the holder gives a name and an email when assigning a ticket, and the attendee answers the event's questions.
access_lockedboolean- Whether the holder must sign in with an address the access list admits. The list itself is not reported.
give_out_untilstring (RFC 3339) or null- The portal's cut-off: after this moment the holder can no longer send links, assign tickets, upload a list or register people on the share link. Tickets already given out, and links already sent, keep working. null for no cut-off.
archivedbooleanrevokedbooleancreated_atstring (RFC 3339) or null
Responses
200The portal as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/portals/1/unarchive \
-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/portals/1/unarchive", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"ref": "P7K2QM",
"slug": "amgen-k8wa6",
"name": "Amgen",
"url": "https://acct.go.tito.io/annual/portals/1qR20rFnPEV70bqeGNLJZg",
"share_url": null,
"shareable": false,
"share_ticket_type_ids": [
12,
14
],
"contact_email": "sponsor@example.com",
"currency": "eur",
"allocation": 100,
"per_type": false,
"ticket_types": [
{
"allocation": 0,
"id": 8
}
],
"issued": 12,
"pending": 3,
"remaining": 85,
"ref_prefix": "SPN",
"package": "Gold stand",
"package_cents": 250000,
"package_detail": "Two passes and a stand",
"package_due_date": "2030-03-01",
"package_tax_rate_id": 0,
"package_price_includes_tax": false,
"capabilities": [
"offerings"
],
"access_locked": false,
"give_out_until": null,
"archived": false,
"revoked": false,
"sent_at": null,
"created_at": "2026-09-07T22:00:00Z"
}
Widgets
Make a widget
POST /events/{slug}/widgets
A new pasteable ticket list. Its SLUG is derived from the name and never changes afterwards — it is in markup on somebody else's website, and a slug that moved would break a page the organizer may no longer have access to. It is answered back, because a caller building the snippet needs it.
An empty STORED ticket-type list means EVERYTHING ON SALE, including types made later — never "lists nothing". So: naming ticket_type_ids narrows the widget to those, in that order; scope: "all" is the way back to everything and the only thing that says it; an empty array is refused, because storing it would widen the widget to the exact opposite of what was asked; and an explicit null is refused the way every other field's is.
secured_line is on unless switched off; analytics is off until asked for, because pushing into somebody's analytics is something they ask for rather than something we start doing for them. The setup walkthrough is not here: it is guidance for a person pasting the snippet into a website, and a widget made through the API exists the moment it is made.
Requires events.manage
Path parameters
slugstring required- The event's slug.
Request body
shapestring- A property of the record, so a widget changes shape without anybody touching the website it is on.
- one of
listbutton all_ticket_types_linkboolean- When the widget lists only some ticket types, end its list with an "All ticket types" link that reopens it showing every ticket type on sale, keeping the quantities already picked. It does nothing on a widget that lists everything. Off by default. It is the same switch as "Let people see every ticket type on sale" on the widget's page in the admin.
full_page_linkboolean- Show an "Open the full page" link in the footer of the widget's checkout, which carries the buyer to the same step on the event's own Tito page, holding the same order. On by default.
show_promo_fieldboolean- Show the promotion code field in the widget, on its ticket list, its order review and the booking form reached from it. Codes still work from their links with it off. On by default. It is the same switch as "Show the promotion code field" on the widget's page in the admin.
prefill_reviewboolean- When the website hands over a buyer's details with tito("widget.create", …), show the filled-in form first instead of going straight on to paying. Off by default. It is the same choice as "Details from your website" on the widget's page in the admin.
openingstring- Accepted and ignored. Every widget opens with
push, whatever is sent here, so a request written whendepthandblurwere choices still succeeds. scopestringalllists everything on sale and is the only way back to it. Naming ticket_type_ids already meanschosen, so this can be left out;allwith a list beside it is refused.- one of
allchosen ticket_type_idsarray of integer- The types to list, in the order to list them. Empty is refused; null is refused.
interest_messagestringbannerstring- What the widget shows across the top of its ticket list:
""for no banner,"event"for the event's own banner (followed as it changes), an https image URL, or"asset:<id>", a picture uploaded on the widget's page in the admin. A picture of the widget's own is uploaded in the admin; here, pass a URL,"event","", or anasset:value the widget already has.
Response fields
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 types it lists, in the order it lists them. EMPTY IS EVERYTHING ON SALE, including types made later — not "lists nothing".
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)analyticsboolean- Hands the widget's own events to the tracking already on the organizer's page. Off unless asked for.
all_ticket_types_linkboolean- When the widget lists only some ticket types, its list ends with an "All ticket types" link that reopens it showing every ticket type on sale, keeping the quantities already picked. Off unless asked for.
full_page_linkboolean- Whether the widget's checkout shows an "Open the full page" link in its footer, which carries the buyer to the same step on the event's own Tito page, holding the same order. On unless switched off.
show_promo_fieldboolean- Whether the widget shows the promotion code field, on its ticket list, its order review and the booking form reached from it. Only an event with codes that can be used shows it at all. On unless switched off. The event page's own field is set in Customize.
prefill_reviewboolean- When the website the widget sits on hands over a buyer's details with tito("widget.create", …), whether the widget shows the filled-in form first. Off, the buyer goes straight on to paying once nothing the event asks is missing. Off unless switched on.
openingstring- How the widget opens over the organizer's page. Always
push: the page pushes back as one rounded card and the tickets rise over it. Visitors who ask their device for less motion get a fade. - one of
push interest_messagestringlegacy_tokenstring- The Tito Pro widget token this widget also answers to, so a website moving from Tito Pro keeps the
data-tito-tokenandtito("widget.open", …)it already has. Empty for a widget made in Tito Go. Carried over by the account import and never set through this API. An address the website already prints, not a credential. bannerstring- What the widget shows across the top of its ticket list:
""for no banner,"event"for the event's own banner (followed as it changes), an https image URL, or"asset:<id>", a picture uploaded on the widget's page in the admin. show_dateboolean- Shows the event's date and time above the ticket list, in the event's own time zone. Off by default.
show_venueboolean- Shows the venue's name and address above the ticket list, the address opening a map in a new tab. Off by default.
Responses
201The widget as made.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The widget cannot be saved (invalid_body): a blank name;ticket_type_idsempty, or naming a type that is not this event's; ascopethat is neither word; orscope: "all"with a type list beside it.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/widgets \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Winter sessions"}'
// 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: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Winter sessions"
}),
});
const data = await res.json();
{
"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",
"analytics": false,
"share_buyer": false,
"all_ticket_types_link": false,
"full_page_link": true,
"show_promo_field": true,
"prefill_review": false,
"opening": "push",
"purpose": "purchase",
"interest_message": "example",
"legacy_token": "",
"banner": "event",
"show_event_name": false,
"show_date": false,
"show_venue": false,
"show_descriptions": true
}
Delete a widget
DELETE /events/{slug}/widgets/{id}
This breaks a live page on somebody's website, which is why the organizer's own screen puts a confirmation in front of it; a caller gets none, because a confirmation is a person being asked. The widget's colours go with it: a deleted widget's draft is nobody's to publish. The websites it was seen on are the account's and stay.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The widget's id, as
GET /events/{slug}/widgetslists it.
Response fields
deletedboolean
Responses
200It is gone.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/widgets/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/widgets/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"deleted": true
}
Get a widget
GET /events/{slug}/widgets/{id}
One widget, in the list's shape.
Requires events.view
Path parameters
slugstring required- The event's slug.
idinteger required- The widget's id, as
GET /events/{slug}/widgetslists it.
Response fields
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 types it lists, in the order it lists them. EMPTY IS EVERYTHING ON SALE, including types made later — not "lists nothing".
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)analyticsboolean- Hands the widget's own events to the tracking already on the organizer's page. Off unless asked for.
all_ticket_types_linkboolean- When the widget lists only some ticket types, its list ends with an "All ticket types" link that reopens it showing every ticket type on sale, keeping the quantities already picked. Off unless asked for.
full_page_linkboolean- Whether the widget's checkout shows an "Open the full page" link in its footer, which carries the buyer to the same step on the event's own Tito page, holding the same order. On unless switched off.
show_promo_fieldboolean- Whether the widget shows the promotion code field, on its ticket list, its order review and the booking form reached from it. Only an event with codes that can be used shows it at all. On unless switched off. The event page's own field is set in Customize.
prefill_reviewboolean- When the website the widget sits on hands over a buyer's details with tito("widget.create", …), whether the widget shows the filled-in form first. Off, the buyer goes straight on to paying once nothing the event asks is missing. Off unless switched on.
openingstring- How the widget opens over the organizer's page. Always
push: the page pushes back as one rounded card and the tickets rise over it. Visitors who ask their device for less motion get a fade. - one of
push interest_messagestringlegacy_tokenstring- The Tito Pro widget token this widget also answers to, so a website moving from Tito Pro keeps the
data-tito-tokenandtito("widget.open", …)it already has. Empty for a widget made in Tito Go. Carried over by the account import and never set through this API. An address the website already prints, not a credential. bannerstring- What the widget shows across the top of its ticket list:
""for no banner,"event"for the event's own banner (followed as it changes), an https image URL, or"asset:<id>", a picture uploaded on the widget's page in the admin. show_dateboolean- Shows the event's date and time above the ticket list, in the event's own time zone. Off by default.
show_venueboolean- Shows the venue's name and address above the ticket list, the address opening a map in a new tab. Off by default.
Responses
200The 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/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/widgets/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"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",
"analytics": false,
"share_buyer": false,
"all_ticket_types_link": false,
"full_page_link": true,
"show_promo_field": true,
"prefill_review": false,
"opening": "push",
"purpose": "purchase",
"interest_message": "example",
"legacy_token": "",
"banner": "event",
"show_event_name": false,
"show_date": false,
"show_venue": false,
"show_descriptions": true
}
Edit a widget
PATCH /events/{slug}/widgets/{id}
Only the keys present change. The slug is not among them and never moves.
An empty STORED ticket-type list means EVERYTHING ON SALE, including types made later — never "lists nothing". So: naming ticket_type_ids narrows the widget to those, in that order; scope: "all" is the way back to everything and the only thing that says it; an empty array is refused, because storing it would widen the widget to the exact opposite of what was asked; and an explicit null is refused the way every other field's is.
An unknown shape falls back to the one the widget already has rather than being refused — the organizer's own form does the same, because a pair of radios has no blank option.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The widget's id, as
GET /events/{slug}/widgetslists it.
Request body
shapestring- A property of the record, so a widget changes shape without anybody touching the website it is on.
- one of
listbutton all_ticket_types_linkboolean- When the widget lists only some ticket types, end its list with an "All ticket types" link that reopens it showing every ticket type on sale, keeping the quantities already picked. It does nothing on a widget that lists everything. Off by default. It is the same switch as "Let people see every ticket type on sale" on the widget's page in the admin.
full_page_linkboolean- Show an "Open the full page" link in the footer of the widget's checkout, which carries the buyer to the same step on the event's own Tito page, holding the same order. On by default.
show_promo_fieldboolean- Show the promotion code field in the widget, on its ticket list, its order review and the booking form reached from it. Codes still work from their links with it off. On by default. It is the same switch as "Show the promotion code field" on the widget's page in the admin.
prefill_reviewboolean- When the website hands over a buyer's details with tito("widget.create", …), show the filled-in form first instead of going straight on to paying. Off by default. It is the same choice as "Details from your website" on the widget's page in the admin.
openingstring- Accepted and ignored. Every widget opens with
push, whatever is sent here, so a request written whendepthandblurwere choices still succeeds. scopestringalllists everything on sale and is the only way back to it. Naming ticket_type_ids already meanschosen, so this can be left out;allwith a list beside it is refused.- one of
allchosen ticket_type_idsarray of integer- The types to list, in the order to list them. Empty is refused; null is refused.
interest_messagestringbannerstring- What the widget shows across the top of its ticket list:
""for no banner,"event"for the event's own banner (followed as it changes), an https image URL, or"asset:<id>", a picture uploaded on the widget's page in the admin. A picture of the widget's own is uploaded in the admin; here, pass a URL,"event","", or anasset:value the widget already has.
Response fields
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 types it lists, in the order it lists them. EMPTY IS EVERYTHING ON SALE, including types made later — not "lists nothing".
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)analyticsboolean- Hands the widget's own events to the tracking already on the organizer's page. Off unless asked for.
all_ticket_types_linkboolean- When the widget lists only some ticket types, its list ends with an "All ticket types" link that reopens it showing every ticket type on sale, keeping the quantities already picked. Off unless asked for.
full_page_linkboolean- Whether the widget's checkout shows an "Open the full page" link in its footer, which carries the buyer to the same step on the event's own Tito page, holding the same order. On unless switched off.
show_promo_fieldboolean- Whether the widget shows the promotion code field, on its ticket list, its order review and the booking form reached from it. Only an event with codes that can be used shows it at all. On unless switched off. The event page's own field is set in Customize.
prefill_reviewboolean- When the website the widget sits on hands over a buyer's details with tito("widget.create", …), whether the widget shows the filled-in form first. Off, the buyer goes straight on to paying once nothing the event asks is missing. Off unless switched on.
openingstring- How the widget opens over the organizer's page. Always
push: the page pushes back as one rounded card and the tickets rise over it. Visitors who ask their device for less motion get a fade. - one of
push interest_messagestringlegacy_tokenstring- The Tito Pro widget token this widget also answers to, so a website moving from Tito Pro keeps the
data-tito-tokenandtito("widget.open", …)it already has. Empty for a widget made in Tito Go. Carried over by the account import and never set through this API. An address the website already prints, not a credential. bannerstring- What the widget shows across the top of its ticket list:
""for no banner,"event"for the event's own banner (followed as it changes), an https image URL, or"asset:<id>", a picture uploaded on the widget's page in the admin. show_dateboolean- Shows the event's date and time above the ticket list, in the event's own time zone. Off by default.
show_venueboolean- Shows the venue's name and address above the ticket list, the address opening a map in a new tab. Off by default.
Responses
200The widget as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The widget cannot be saved (invalid_body): a blank name;ticket_type_idsempty, or naming a type that is not this event's; ascopethat is neither word; orscope: "all"with a type list beside it.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/widgets/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Winter sessions"}'
// 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/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Winter sessions"
}),
});
const data = await res.json();
{
"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",
"analytics": false,
"share_buyer": false,
"all_ticket_types_link": false,
"full_page_link": true,
"show_promo_field": true,
"prefill_review": false,
"opening": "push",
"purpose": "purchase",
"interest_message": "example",
"legacy_token": "",
"banner": "event",
"show_event_name": false,
"show_date": false,
"show_venue": false,
"show_descriptions": true
}
Ticket requests
List application pages
GET /events/{slug}/application-pages
An application page is a public form people fill in to ask for a place; an application is one such request, which the organiser approves or turns down. A page decides by review (one at a time), by draw or by threshold, and takes payment by invitation or by a card saved on the form. Decisions are recorded in the event's activity against the key. In the order they were made.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
Response fields
application_pagesarray of ApplicationPageapplication_pages[].public_headingstring- What requesters read at the top; "" shows the event's name.
application_pages[].public_descriptionstring- Plain text shown under the heading; a blank line starts a new paragraph. "" shows nothing there.
application_pages[].pay_modestring- invite (pay on claiming) or card (a card saved on the form, charged on approval).
- one of
invitecard application_pages[].closes_atstring (RFC 3339) or null- When the page stops taking requests; null for no date.
application_pages[].approvedintegerapplication_pages[].endedboolean- Whether everybody still waiting has had an answer (draw and threshold pages).
application_pages[].created_atstring (RFC 3339) or null
Responses
200Application pages.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/application-pages \
-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/application-pages", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"application_pages": [
{
"id": 2,
"slug": "students",
"url": "https://acct.go.tito.io/annual/request/students",
"name": "Student places",
"public_heading": "Ask for a student place",
"public_description": "Places are for full-time students.\n\nBring your student card on the day.",
"decide_by": "review",
"pay_mode": "invite",
"ticket_type_ids": [
8
],
"max_quantity": 4,
"closes_at": null,
"closed": false,
"reject_email": true,
"reject_message": "Sorry, not this time.",
"draw_places": 0,
"target_count": 0,
"waiting": 3,
"applied": 12,
"approved": 7,
"not_accepted": 1,
"withdrawn": 1,
"ended": false,
"created_at": "2026-09-07T22:40:00Z"
}
]
}
Create an application page
POST /events/{slug}/application-pages
A new page and the invitation list behind it. The slug derives from the name when blank; decide_by defaults to review and pay_mode to invite.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
Request body
namestringpublic_descriptionstring- Plain text under the heading; a blank line starts a new paragraph. "" for none.
decide_bystring- review (default), draw or threshold. Fixed once anybody has applied.
- one of
reviewdrawthreshold reject_messagestring
Response fields
public_descriptionstring- Plain text shown under the heading; a blank line starts a new paragraph. "" shows nothing there.
pay_modestring- invite (pay on claiming) or card (a card saved on the form, charged on approval).
- one of
invitecard approvedintegercreated_atstring (RFC 3339) or null
Responses
201The new page.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Another page already has that slug on this event (slug_taken).422The body was unreadable or carried an unknown field, the name was blank, no ticket type or one not this event's,max_quantityoutside 1–100, an unknowndecide_byorpay_mode, a draw withoutdraw_places, a threshold withouttarget_countorcloses_at, an unreadablecloses_at, orreject_emailwith noreject_message(invalid_body); a malformed slug (invalid_slug).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/application-pages \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Student places"}'
// 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/application-pages", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Student places"
}),
});
const data = await res.json();
{
"id": 2,
"slug": "students",
"url": "https://acct.go.tito.io/annual/request/students",
"name": "Student places",
"public_heading": "Ask for a student place",
"public_description": "Places are for full-time students.\n\nBring your student card on the day.",
"decide_by": "review",
"pay_mode": "invite",
"ticket_type_ids": [
8
],
"max_quantity": 4,
"closes_at": null,
"closed": false,
"reject_email": true,
"reject_message": "Sorry, not this time.",
"draw_places": 0,
"target_count": 0,
"waiting": 3,
"applied": 12,
"approved": 7,
"not_accepted": 1,
"withdrawn": 1,
"ended": false,
"created_at": "2026-09-07T22:40:00Z"
}
Get an application page
GET /events/{slug}/application-pages/{id}
One page, with its band of counts.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
idinteger required- The page's id, as
GET /events/{slug}/application-pageslists it.
Response fields
public_descriptionstring- Plain text shown under the heading; a blank line starts a new paragraph. "" shows nothing there.
pay_modestring- invite (pay on claiming) or card (a card saved on the form, charged on approval).
- one of
invitecard approvedintegercreated_atstring (RFC 3339) or null
Responses
200The page.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/application-pages/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/application-pages/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 2,
"slug": "students",
"url": "https://acct.go.tito.io/annual/request/students",
"name": "Student places",
"public_heading": "Ask for a student place",
"public_description": "Places are for full-time students.\n\nBring your student card on the day.",
"decide_by": "review",
"pay_mode": "invite",
"ticket_type_ids": [
8
],
"max_quantity": 4,
"closes_at": null,
"closed": false,
"reject_email": true,
"reject_message": "Sorry, not this time.",
"draw_places": 0,
"target_count": 0,
"waiting": 3,
"applied": 12,
"approved": 7,
"not_accepted": 1,
"withdrawn": 1,
"ended": false,
"created_at": "2026-09-07T22:40:00Z"
}
Update an application page
PATCH /events/{slug}/application-pages/{id}
The setup form's fields; absent ones are unchanged, an explicit null and an unknown field are refused. ticket_type_ids, when sent, replaces the offered set. decide_by and pay_mode are fixed once anybody has applied.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The page's id, as
GET /events/{slug}/application-pageslists it.
Request body
namestringpublic_descriptionstring- Plain text under the heading; a blank line starts a new paragraph. "" for none.
decide_bystring- review (default), draw or threshold. Fixed once anybody has applied.
- one of
reviewdrawthreshold reject_messagestring
Response fields
public_descriptionstring- Plain text shown under the heading; a blank line starts a new paragraph. "" shows nothing there.
pay_modestring- invite (pay on claiming) or card (a card saved on the form, charged on approval).
- one of
invitecard approvedintegercreated_atstring (RFC 3339) or null
Responses
200The page as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The slug is another page's (slug_taken), ordecide_by/pay_modechanged after somebody applied (applied).422As for create (invalid_body,invalid_slug).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/application-pages/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Student places"}'
// 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/application-pages/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Student places"
}),
});
const data = await res.json();
{
"id": 2,
"slug": "students",
"url": "https://acct.go.tito.io/annual/request/students",
"name": "Student places",
"public_heading": "Ask for a student place",
"public_description": "Places are for full-time students.\n\nBring your student card on the day.",
"decide_by": "review",
"pay_mode": "invite",
"ticket_type_ids": [
8
],
"max_quantity": 4,
"closes_at": null,
"closed": false,
"reject_email": true,
"reject_message": "Sorry, not this time.",
"draw_places": 0,
"target_count": 0,
"waiting": 3,
"applied": 12,
"approved": 7,
"not_accepted": 1,
"withdrawn": 1,
"ended": false,
"created_at": "2026-09-07T22:40:00Z"
}
List a page's applications
GET /events/{slug}/application-pages/{id}/applications
Everyone who asked, newest first, with their answers to the form's questions. The requester's own link and any card references are never reported.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
idinteger required- The page's id, as
GET /events/{slug}/application-pageslists it.
Response fields
applicationsarray of Applicationapplications[].idintegerapplications[].namestringapplications[].emailstringapplications[].statestring- new, approved, rejected, withdrawn, given (a place marked by hand before a draw runs) or ruledout.
- one of
newapprovedrejectedwithdrawngivenruledout applications[].asked_ticket_type_idinteger or nullapplications[].asked_quantityintegerapplications[].invitation_idinteger or null- The invitation an approval minted; null on a charged approval or before one.
applications[].decided_atstring (RFC 3339) or nullapplications[].card_brandstringapplications[].card_last4stringapplications[].answers[].field_idintegerapplications[].answers[].valuestringapplications[].created_atstring (RFC 3339) or nullapplications[].anonymized_atstring (RFC 3339) or null- When the requester was anonymized, null while they are still on it. An anonymized request keeps its reference, what was asked for and its state, and has no name or email address.
Responses
200Applications.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/application-pages/1/applications \
-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/application-pages/1/applications", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"applications": [
{
"id": 41,
"page_id": 2,
"ref": "R7K2QM",
"name": "Aoife Ní Bhriain",
"email": "aoife@example.com",
"locale": "ga",
"state": "new",
"asked_ticket_type_id": 8,
"asked_quantity": 3,
"granted_ticket_type_id": null,
"granted_quantity": 0,
"invitation_id": null,
"order_ref": null,
"decided_at": null,
"decided_by": "",
"card_state": "",
"card_brand": "",
"card_last4": "",
"charge_failure": "",
"answers": [
{
"field_id": 5,
"value": "UCD"
}
],
"created_at": "2026-09-07T22:41:00Z",
"anonymized_at": "2026-10-14T09:00:00Z"
}
]
}
Close an application page
POST /events/{slug}/application-pages/{id}/close
Stops the page taking requests: the switchboard's switch. The link keeps working and says so.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The page's id, as
GET /events/{slug}/application-pageslists it.
Response fields
public_descriptionstring- Plain text shown under the heading; a blank line starts a new paragraph. "" shows nothing there.
pay_modestring- invite (pay on claiming) or card (a card saved on the form, charged on approval).
- one of
invitecard approvedintegercreated_atstring (RFC 3339) or null
Responses
200The page as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/application-pages/1/close \
-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/application-pages/1/close", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 2,
"slug": "students",
"url": "https://acct.go.tito.io/annual/request/students",
"name": "Student places",
"public_heading": "Ask for a student place",
"public_description": "Places are for full-time students.\n\nBring your student card on the day.",
"decide_by": "review",
"pay_mode": "invite",
"ticket_type_ids": [
8
],
"max_quantity": 4,
"closes_at": null,
"closed": false,
"reject_email": true,
"reject_message": "Sorry, not this time.",
"draw_places": 0,
"target_count": 0,
"waiting": 3,
"applied": 12,
"approved": 7,
"not_accepted": 1,
"withdrawn": 1,
"ended": false,
"created_at": "2026-09-07T22:40:00Z"
}
Reopen an application page
POST /events/{slug}/application-pages/{id}/reopen
Flicks the switch back; a closing date that has passed still closes it.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The page's id, as
GET /events/{slug}/application-pageslists it.
Response fields
public_descriptionstring- Plain text shown under the heading; a blank line starts a new paragraph. "" shows nothing there.
pay_modestring- invite (pay on claiming) or card (a card saved on the form, charged on approval).
- one of
invitecard approvedintegercreated_atstring (RFC 3339) or null
Responses
200The page as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/application-pages/1/reopen \
-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/application-pages/1/reopen", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 2,
"slug": "students",
"url": "https://acct.go.tito.io/annual/request/students",
"name": "Student places",
"public_heading": "Ask for a student place",
"public_description": "Places are for full-time students.\n\nBring your student card on the day.",
"decide_by": "review",
"pay_mode": "invite",
"ticket_type_ids": [
8
],
"max_quantity": 4,
"closes_at": null,
"closed": false,
"reject_email": true,
"reject_message": "Sorry, not this time.",
"draw_places": 0,
"target_count": 0,
"waiting": 3,
"applied": 12,
"approved": 7,
"not_accepted": 1,
"withdrawn": 1,
"ended": false,
"created_at": "2026-09-07T22:40:00Z"
}
Get an application
GET /events/{slug}/applications/{id}
One request.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
idinteger required- The application's id, as a page's applications list it.
Response fields
idintegernamestringemailstringstatestring- new, approved, rejected, withdrawn, given (a place marked by hand before a draw runs) or ruledout.
- one of
newapprovedrejectedwithdrawngivenruledout asked_ticket_type_idinteger or nullasked_quantityintegerinvitation_idinteger or null- The invitation an approval minted; null on a charged approval or before one.
decided_atstring (RFC 3339) or nullcard_brandstringcard_last4stringanswers[].field_idintegeranswers[].valuestringcreated_atstring (RFC 3339) or nullanonymized_atstring (RFC 3339) or null- When the requester was anonymized, null while they are still on it. An anonymized request keeps its reference, what was asked for and its state, and has no name or email address.
Responses
200The application.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/applications/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/applications/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 41,
"page_id": 2,
"ref": "R7K2QM",
"name": "Aoife Ní Bhriain",
"email": "aoife@example.com",
"locale": "ga",
"state": "new",
"asked_ticket_type_id": 8,
"asked_quantity": 3,
"granted_ticket_type_id": null,
"granted_quantity": 0,
"invitation_id": null,
"order_ref": null,
"decided_at": null,
"decided_by": "",
"card_state": "",
"card_brand": "",
"card_last4": "",
"charge_failure": "",
"answers": [
{
"field_id": 5,
"value": "UCD"
}
],
"created_at": "2026-09-07T22:41:00Z",
"anonymized_at": "2026-10-14T09:00:00Z"
}
Anonymize a person who requested a ticket
POST /events/{slug}/applications/{id}/anonymize
Remove the person from one ticket request: their name and email address, the card details shown for it, and every answer they gave. A request still waiting on a decision is withdrawn (state becomes withdrawn) and the card saved with it is deleted at Stripe, as when the person withdraws it themselves. The invitation it was approved into, and the order that invitation was claimed with, are not changed and are named in kept.invitation and kept.order; anonymize them with POST /events/{slug}/invitations/{id}/anonymize and POST /orders/{ref}/anonymize. What stays: the request itself with its reference, what was asked for and given, and its state. It happens at once and cannot be undone, the API's copy of the admin's Anonymize confirm step, which runs the same code. Copies of the person in sent emails, the record's history, its webhook deliveries, workflow runs and the API request log are cleared too, and their email address comes off every event's mailing list and out of Contacts. Every call writes a line to the data-protection log (GET /data-protection-log) that names the record's reference and never the person. Send no request body; any body is ignored.
Requires erasure
Path parameters
slugstring required- The event's slug.
idinteger required- The application's id, as a page's applications list it.
Response fields
refstring- The record's reference: a ticket, order, customer, waitlist entry or ticket request ref, or the invitation's id.
anonymized_atstring (RFC 3339)removedobjectremoved.emailsinteger- Sent emails whose address and words were removed. The record that each was sent stays.
removed.link_tagsinteger- Orders whose link tags were removed: every tag that isn't a
utm_*campaign tag, ad click ids included. removed.interestboolean- Their email address was taken off the mailing list, where they signed up to hear when tickets go on sale.
removed.cardboolean- A card saved with a ticket request this withdrew, which Stripe was asked to delete.
keptobjectkept.other_recordsinteger- Other orders, tickets and invitations with the same email address, which are not changed.
kept.customerstring or null- The customer record an anonymized order is linked to, which is not changed.
kept.campaign_tagsinteger- Orders whose
utm_*campaign tags were kept, so they still count toward the link that sold them. kept.orderstring or null- The order a waitlist place was taken with, or that a ticket request's invitation was claimed with, which is not changed.
kept.invitationinteger or null- The invitation a ticket request was approved into, which is not changed.
outside_titoobjectoutside_tito.payment_providerstring or null- The provider that keeps its own record of the payment, with the buyer's name and email address.
- one of
StripePayPal<nil> outside_tito.servicesarray of string- Connected services the person's details were sent to, which keep their own copy.
outside_tito.cardboolean- A card saved with a ticket request that was already decided, which Stripe keeps.
Responses
200The person who requested a ticket is anonymized.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The person on this record has already been anonymized (already_anonymized). Nothing changes.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/applications/1/anonymize \
-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/applications/1/anonymize", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"anonymized": true,
"kind": "ticket",
"ref": "H7K2PX",
"anonymized_at": "2026-10-14T09:00:00Z",
"removed": {
"tickets": 1,
"orders": 1,
"quotes": 1,
"answers": 1,
"files": 1,
"emails": 1,
"contact": true,
"waitlist": true,
"ticket_request": true,
"link_tags": 1,
"interest": true,
"card": true
},
"kept": {
"documents": 1,
"other_records": 1,
"customer": "example",
"campaign_tags": 1,
"order": "H7K2PX",
"ticket": "T9QW4M",
"invitation": 1
},
"outside_tito": {
"payment_provider": "Stripe",
"services": [
"example"
],
"webhooks": true,
"card": true
}
}
Approve an application
POST /events/{slug}/applications/{id}/approve
Grants the place — as asked, or narrowed to another type the page offers and a smaller number. On a page holding a card the card is charged and the ticket issued outright (outcome: charged); otherwise, and when the card declines, an invitation is minted and emailed (outcome: invited, card_declined saying which). Only a new application is decided.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The application's id, as a page's applications list it.
Request body
Response fields
application.idintegerapplication.namestringapplication.emailstringapplication.statestring- new, approved, rejected, withdrawn, given (a place marked by hand before a draw runs) or ruledout.
- one of
newapprovedrejectedwithdrawngivenruledout application.asked_ticket_type_idinteger or nullapplication.asked_quantityintegerapplication.invitation_idinteger or null- The invitation an approval minted; null on a charged approval or before one.
application.decided_atstring (RFC 3339) or nullapplication.card_brandstringapplication.card_last4stringapplication.answers[].field_idintegerapplication.answers[].valuestringapplication.created_atstring (RFC 3339) or nullapplication.anonymized_atstring (RFC 3339) or null- When the requester was anonymized, null while they are still on it. An anonymized request keeps its reference, what was asked for and its state, and has no name or email address.
card_declinedboolean
Responses
200Decided.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The application is already decided (decided).422A type the page does not offer, or a quantity outside 1 to what was asked (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/applications/1/approve \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"ticket_type_id":8}'
// 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/applications/1/approve", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"ticket_type_id": 8
}),
});
const data = await res.json();
{
"application": {
"id": 41,
"page_id": 2,
"ref": "R7K2QM",
"name": "Aoife Ní Bhriain",
"email": "aoife@example.com",
"locale": "ga",
"state": "new",
"asked_ticket_type_id": 8,
"asked_quantity": 3,
"granted_ticket_type_id": null,
"granted_quantity": 0,
"invitation_id": null,
"order_ref": null,
"decided_at": null,
"decided_by": "",
"card_state": "",
"card_brand": "",
"card_last4": "",
"charge_failure": "",
"answers": [
{
"field_id": 5,
"value": "UCD"
}
],
"created_at": "2026-09-07T22:41:00Z",
"anonymized_at": "2026-10-14T09:00:00Z"
},
"outcome": "invited",
"card_declined": false
}
Reject an application
POST /events/{slug}/applications/{id}/reject
Turns the person down. The page's rejection email goes out when the page has it switched on, unless email: false skips this one; any saved card is forgotten. Only a new application is decided.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The application's id, as a page's applications list it.
Request body
emailboolean- false skips this one person's email on a page that sends them; true is refused on a page that does not.
Response fields
application.idintegerapplication.namestringapplication.emailstringapplication.statestring- new, approved, rejected, withdrawn, given (a place marked by hand before a draw runs) or ruledout.
- one of
newapprovedrejectedwithdrawngivenruledout application.asked_ticket_type_idinteger or nullapplication.asked_quantityintegerapplication.invitation_idinteger or null- The invitation an approval minted; null on a charged approval or before one.
application.decided_atstring (RFC 3339) or nullapplication.card_brandstringapplication.card_last4stringapplication.answers[].field_idintegerapplication.answers[].valuestringapplication.created_atstring (RFC 3339) or nullapplication.anonymized_atstring (RFC 3339) or null- When the requester was anonymized, null while they are still on it. An anonymized request keeps its reference, what was asked for and its state, and has no name or email address.
emailedboolean
Responses
200Decided.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The application is already decided (decided).422email: trueon a page whose rejection email is off (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/applications/1/reject \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"email":false}'
// 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/applications/1/reject", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"email": false
}),
});
const data = await res.json();
{
"application": {
"id": 41,
"page_id": 2,
"ref": "R7K2QM",
"name": "Aoife Ní Bhriain",
"email": "aoife@example.com",
"locale": "ga",
"state": "new",
"asked_ticket_type_id": 8,
"asked_quantity": 3,
"granted_ticket_type_id": null,
"granted_quantity": 0,
"invitation_id": null,
"order_ref": null,
"decided_at": null,
"decided_by": "",
"card_state": "",
"card_brand": "",
"card_last4": "",
"charge_failure": "",
"answers": [
{
"field_id": 5,
"value": "UCD"
}
],
"created_at": "2026-09-07T22:41:00Z",
"anonymized_at": "2026-10-14T09:00:00Z"
},
"emailed": true
}
Content
Read event page order
GET /events/{slug}/page-order
Tokens identify stored sections (sID), ticket types (tID), and products (pID). Each section groups following items until the next section. Includes empty hidden default sections. Names use the default language for platform headings.
Requires events.view
Path parameters
slugstring required- The event's slug.
Response fields
orderarray of stringmax_sectionsintegersectionsarray of objectsections[].tokenstringsections[].idintegersections[].namestringitemsarray of objectitems[].tokenstringitems[].namestringitems[].idinteger
Responses
200Current event page order.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/page-order \
-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/page-order", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"order": [
"H7K2PX"
],
"max_sections": 1,
"sections": [
{
"token": "example",
"id": 1,
"name": "Ada Lovelace",
"hidden": true
}
],
"items": [
{
"token": "example",
"name": "Ada Lovelace",
"kind": "tickets",
"id": 1,
"family": "ticket_type"
}
]
}
Arrange the event page
PUT /events/{slug}/page-order
Send current event tokens and nN tokens for new sections, starting with a section. Omitted sections are removed while their contents are preserved. Omitted items remain in their current or previous surviving section; hidden default sections can also be moved or removed. At most 30 sections including hidden defaults. Names are optional, single-line and truncated to 80 characters. A blank name clears a heading; omitted names preserve existing headings. Saving is transactional.
Requires events.manage
Path parameters
slugstring required- The event's slug.
Request body
Response fields
orderarray of stringmax_sectionsintegersectionsarray of objectsections[].tokenstringsections[].idintegersections[].namestringitemsarray of objectitems[].tokenstringitems[].namestringitems[].idinteger
Responses
200Current event page order.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid, duplicate, foreign, or excessive tokens; invalid section names.500Internal error.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/events/annual-conf/page-order \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"order":["H7K2PX"]}'
// 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/page-order", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"order": [
"H7K2PX"
]
}),
});
const data = await res.json();
{
"order": [
"H7K2PX"
],
"max_sections": 1,
"sections": [
{
"token": "example",
"id": 1,
"name": "Ada Lovelace",
"hidden": true
}
],
"items": [
{
"token": "example",
"name": "Ada Lovelace",
"kind": "tickets",
"id": 1,
"family": "ticket_type"
}
]
}
List the Content pages
GET /events/{slug}/pages
The event's Content pages: standalone pages with an address of their own — the FAQs, how to get there, what sponsors get. They are not the account's custom pages, which are a developer feature (raw account-authored HTML, own host only); a Content page stores STRUCTURE, so it renders on the platform root domain too. Pieces whose address is switched off are included: a shut address is a decision about the address, not a piece that is missing.
Requires events.view
Path parameters
slugstring required- The event's slug.
Response fields
pagesarray of StaticPagepages[].idintegerpages[].urlstring- Where it renders, in the URL shape this request arrived on. With
publishedfalse the piece has this address but it does not open. pages[].bodystring- The page as prose: the composer's variables read as their fallbacks, its blocks and marks dropped. What a caller writes and what it gets back.
pages[].publishedboolean- Whether the piece's own address opens, and only that. Wherever the piece is placed on a screen or in an email, it shows either way.
pages[].ticket_type_idsarray of integer- Who reads the piece where a reader holds tickets. Empty is everyone. Otherwise the order page and the order confirmation show it only for an order with a ticket of one of these types, and the ticket page and the ticket email only for a ticket of one of them. The event page, the booking form and the piece's own address show it to everyone.
pages[].has_unpublished_changesboolean- Whether somebody has words for this piece in the admin's editor that are not published yet.
titleandbodyare always the published words. While this is true, a write to the page is refused with409 unpublished_changes. pages[].portal_placementsarray of PortalPlacement- Where the piece shows in portals. Set with
POST /events/{slug}/pages/{id}/portal-placements. pages[].portal_placements[].idintegerpages[].portal_placements[].pagestring- The portal page it sits on.
ticketsis the portal's own link, where the ticket form is.ownis the page as a page of its own in the portal's menu, after Tickets, at the portal's link plus its style name.own:<id>is the page of its own of page<id>. A portal that does not draw the page shows nothing. pages[].portal_placements[].positionstring- The top or the bottom of that page. Always
topforown. Onticketsit can also bebefore_issue, just above the portal's Issue tickets heading. - one of
topbefore_issuebottom pages[].portal_placements[].every_portalboolean- Every portal of the event, including portals made later, less
hidden_from_portal_ids. Otherwise it shows inportal_idsonly. pages[].portal_placements[].portal_idsarray of integer- The portals it shows in, when
every_portalis false. pages[].created_atstring (RFC 3339) or nullpages[].updated_atstring (RFC 3339) or null
Responses
200The pages.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/pages \
-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/pages", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"pages": [
{
"id": 7,
"path": "getting-here",
"url": "https://acct.go.tito.io/summit/getting-here",
"title": "Getting to the venue",
"body": "The green line stops at St Stephen's Green.",
"body_source": "The green line stops at St Stephen's Green.",
"published": true,
"ticket_type_ids": [],
"has_unpublished_changes": false,
"portal_placements": [
{
"every_portal": true,
"hidden_from_portal_ids": [],
"id": 4,
"page": "tickets",
"portal_ids": [],
"position": "top"
}
],
"created_at": "2026-09-08T12:20:00Z",
"updated_at": "2026-09-08T12:20:00Z"
}
]
}
Write a Content page
POST /events/{slug}/pages
A page arrives a DRAFT unless published says otherwise: publishing is a decision, and defaulting to live would make it an accident. body is PROSE. It lands through the composer's own serializer, so a posted <figh-include>, <script> or {{ mark.bold }} is stored and rendered as text and never as markup, a directive or a mark — that is what allows these pages on the root domain. The composer's variables, blocks and marks are not authored here; body_source answers back what the admin wrote, verbatim.
Requires events.manage
Path parameters
slugstring required- The event's slug.
Request body
pathstring- Optional. Lowercase letters, digits and dashes; one segment, and not a word a real route already answers at. Leave it out and the piece has no address of its own: it is a block, shown where it is placed rather than at a URL.
titlestring- Optional. What the piece is called in the admin, kept to one line. Leave it empty and the admin lists the piece by the start of its words.
publishedboolean- Whether the piece's own address opens. It decides nothing about where the piece is placed. Absent on a create means the address stays shut.
ticket_type_idsarray of integer- Optional. The ticket types the piece is for, each one of this event's. Replaces the stored list. An empty array makes it for everyone again.
Response fields
idintegerurlstring- Where it renders, in the URL shape this request arrived on. With
publishedfalse the piece has this address but it does not open. bodystring- The page as prose: the composer's variables read as their fallbacks, its blocks and marks dropped. What a caller writes and what it gets back.
publishedboolean- Whether the piece's own address opens, and only that. Wherever the piece is placed on a screen or in an email, it shows either way.
ticket_type_idsarray of integer- Who reads the piece where a reader holds tickets. Empty is everyone. Otherwise the order page and the order confirmation show it only for an order with a ticket of one of these types, and the ticket page and the ticket email only for a ticket of one of them. The event page, the booking form and the piece's own address show it to everyone.
has_unpublished_changesboolean- Whether somebody has words for this piece in the admin's editor that are not published yet.
titleandbodyare always the published words. While this is true, a write to the page is refused with409 unpublished_changes. portal_placementsarray of PortalPlacement- Where the piece shows in portals. Set with
POST /events/{slug}/pages/{id}/portal-placements. portal_placements[].idintegerportal_placements[].pagestring- The portal page it sits on.
ticketsis the portal's own link, where the ticket form is.ownis the page as a page of its own in the portal's menu, after Tickets, at the portal's link plus its style name.own:<id>is the page of its own of page<id>. A portal that does not draw the page shows nothing. portal_placements[].positionstring- The top or the bottom of that page. Always
topforown. Onticketsit can also bebefore_issue, just above the portal's Issue tickets heading. - one of
topbefore_issuebottom portal_placements[].every_portalboolean- Every portal of the event, including portals made later, less
hidden_from_portal_ids. Otherwise it shows inportal_idsonly. portal_placements[].portal_idsarray of integer- The portals it shows in, when
every_portalis false. created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or null
Responses
201The page as written.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The page cannot be saved. The errorcodeis the reason:body_too_long,address_invalid(not a slug, or a word a real route already answers at),address_taken_by_audience,address_taken_by_page, orinvalid_bodyfor an unreadable body. One is named at a time, the address first.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/pages \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"path":"getting-here"}'
// 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/pages", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"path": "getting-here"
}),
});
const data = await res.json();
{
"id": 7,
"path": "getting-here",
"url": "https://acct.go.tito.io/summit/getting-here",
"title": "Getting to the venue",
"body": "The green line stops at St Stephen's Green.",
"body_source": "The green line stops at St Stephen's Green.",
"published": true,
"ticket_type_ids": [],
"has_unpublished_changes": false,
"portal_placements": [
{
"every_portal": true,
"hidden_from_portal_ids": [],
"id": 4,
"page": "tickets",
"portal_ids": [],
"position": "top"
}
],
"created_at": "2026-09-08T12:20:00Z",
"updated_at": "2026-09-08T12:20:00Z"
}
Delete a Content page
DELETE /events/{slug}/pages/{id}
Refused while something on the event links to it: a link that names a page is stored as the PAGE, so deleting one out from under a link would leave words pointing at nothing on a page a buyer is reading. Take the link out first — it is refused rather than cascaded because the organizer knows which sentence they meant.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The page's id, as
GET /events/{slug}/pageslists it.
Response fields
deletedboolean
Responses
200It is gone.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Something on this event links to this page (linked).500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/events/annual-conf/pages/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/pages/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"deleted": true
}
Get a Content page
GET /events/{slug}/pages/{id}
One page, draft or live.
Requires events.view
Path parameters
slugstring required- The event's slug.
idinteger required- The page's id, as
GET /events/{slug}/pageslists it.
Response fields
idintegerurlstring- Where it renders, in the URL shape this request arrived on. With
publishedfalse the piece has this address but it does not open. bodystring- The page as prose: the composer's variables read as their fallbacks, its blocks and marks dropped. What a caller writes and what it gets back.
publishedboolean- Whether the piece's own address opens, and only that. Wherever the piece is placed on a screen or in an email, it shows either way.
ticket_type_idsarray of integer- Who reads the piece where a reader holds tickets. Empty is everyone. Otherwise the order page and the order confirmation show it only for an order with a ticket of one of these types, and the ticket page and the ticket email only for a ticket of one of them. The event page, the booking form and the piece's own address show it to everyone.
has_unpublished_changesboolean- Whether somebody has words for this piece in the admin's editor that are not published yet.
titleandbodyare always the published words. While this is true, a write to the page is refused with409 unpublished_changes. portal_placementsarray of PortalPlacement- Where the piece shows in portals. Set with
POST /events/{slug}/pages/{id}/portal-placements. portal_placements[].idintegerportal_placements[].pagestring- The portal page it sits on.
ticketsis the portal's own link, where the ticket form is.ownis the page as a page of its own in the portal's menu, after Tickets, at the portal's link plus its style name.own:<id>is the page of its own of page<id>. A portal that does not draw the page shows nothing. portal_placements[].positionstring- The top or the bottom of that page. Always
topforown. Onticketsit can also bebefore_issue, just above the portal's Issue tickets heading. - one of
topbefore_issuebottom portal_placements[].every_portalboolean- Every portal of the event, including portals made later, less
hidden_from_portal_ids. Otherwise it shows inportal_idsonly. portal_placements[].portal_idsarray of integer- The portals it shows in, when
every_portalis false. created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or null
Responses
200The page.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/pages/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/pages/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 7,
"path": "getting-here",
"url": "https://acct.go.tito.io/summit/getting-here",
"title": "Getting to the venue",
"body": "The green line stops at St Stephen's Green.",
"body_source": "The green line stops at St Stephen's Green.",
"published": true,
"ticket_type_ids": [],
"has_unpublished_changes": false,
"portal_placements": [
{
"every_portal": true,
"hidden_from_portal_ids": [],
"id": 4,
"page": "tickets",
"portal_ids": [],
"position": "top"
}
],
"created_at": "2026-09-08T12:20:00Z",
"updated_at": "2026-09-08T12:20:00Z"
}
Edit a Content page
PATCH /events/{slug}/pages/{id}
Only the keys present change, so published is how a page goes live and how it comes back down without touching a word of it. body is PROSE. It lands through the composer's own serializer, so a posted <figh-include>, <script> or {{ mark.bold }} is stored and rendered as text and never as markup, a directive or a mark — that is what allows these pages on the root domain. The composer's variables, blocks and marks are not authored here; body_source answers back what the admin wrote, verbatim.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The page's id, as
GET /events/{slug}/pageslists it.
Request body
pathstring- Optional. Lowercase letters, digits and dashes; one segment, and not a word a real route already answers at. Leave it out and the piece has no address of its own: it is a block, shown where it is placed rather than at a URL.
titlestring- Optional. What the piece is called in the admin, kept to one line. Leave it empty and the admin lists the piece by the start of its words.
publishedboolean- Whether the piece's own address opens. It decides nothing about where the piece is placed. Absent on a create means the address stays shut.
ticket_type_idsarray of integer- Optional. The ticket types the piece is for, each one of this event's. Replaces the stored list. An empty array makes it for everyone again.
Response fields
idintegerurlstring- Where it renders, in the URL shape this request arrived on. With
publishedfalse the piece has this address but it does not open. bodystring- The page as prose: the composer's variables read as their fallbacks, its blocks and marks dropped. What a caller writes and what it gets back.
publishedboolean- Whether the piece's own address opens, and only that. Wherever the piece is placed on a screen or in an email, it shows either way.
ticket_type_idsarray of integer- Who reads the piece where a reader holds tickets. Empty is everyone. Otherwise the order page and the order confirmation show it only for an order with a ticket of one of these types, and the ticket page and the ticket email only for a ticket of one of them. The event page, the booking form and the piece's own address show it to everyone.
has_unpublished_changesboolean- Whether somebody has words for this piece in the admin's editor that are not published yet.
titleandbodyare always the published words. While this is true, a write to the page is refused with409 unpublished_changes. portal_placementsarray of PortalPlacement- Where the piece shows in portals. Set with
POST /events/{slug}/pages/{id}/portal-placements. portal_placements[].idintegerportal_placements[].pagestring- The portal page it sits on.
ticketsis the portal's own link, where the ticket form is.ownis the page as a page of its own in the portal's menu, after Tickets, at the portal's link plus its style name.own:<id>is the page of its own of page<id>. A portal that does not draw the page shows nothing. portal_placements[].positionstring- The top or the bottom of that page. Always
topforown. Onticketsit can also bebefore_issue, just above the portal's Issue tickets heading. - one of
topbefore_issuebottom portal_placements[].every_portalboolean- Every portal of the event, including portals made later, less
hidden_from_portal_ids. Otherwise it shows inportal_idsonly. portal_placements[].portal_idsarray of integer- The portals it shows in, when
every_portalis false. created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or null
Responses
200The page as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409unpublished_changes: the page has unpublished changes in the admin, which hold its address and who can open it as well as its words. Nothing was written. Publish or discard them in the admin, then try again.422The page cannot be saved. The errorcodeis the reason:body_too_long,address_invalid(not a slug, or a word a real route already answers at),address_taken_by_audience,address_taken_by_page, orinvalid_bodyfor an unreadable body. One is named at a time, the address first.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/pages/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"path":"getting-here"}'
// 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/pages/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"path": "getting-here"
}),
});
const data = await res.json();
{
"id": 7,
"path": "getting-here",
"url": "https://acct.go.tito.io/summit/getting-here",
"title": "Getting to the venue",
"body": "The green line stops at St Stephen's Green.",
"body_source": "The green line stops at St Stephen's Green.",
"published": true,
"ticket_type_ids": [],
"has_unpublished_changes": false,
"portal_placements": [
{
"every_portal": true,
"hidden_from_portal_ids": [],
"id": 4,
"page": "tickets",
"portal_ids": [],
"position": "top"
}
],
"created_at": "2026-09-08T12:20:00Z",
"updated_at": "2026-09-08T12:20:00Z"
}
Show a Content page in portals
POST /events/{slug}/pages/{id}/portal-placements
Puts the piece at the top or the bottom of a portal page, in every portal of the event (including portals made later) or in the portals named. The piece itself is unchanged, and can show in several places at once.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The page's id, as
GET /events/{slug}/pageslists it.
Request body
pagestring- The portal page:
tickets,details,documentsordownloads.ownmakes it a page of its own in the portal's menu, which needs the page to have a title.own:<id>is the top or bottom of another page's own page. positionstring- Top or bottom of the page. Not needed for
own.before_issueis just above the Issue tickets heading, onticketsonly. - one of
topbefore_issuebottom
Response fields
idintegerpagestring- The portal page it sits on.
ticketsis the portal's own link, where the ticket form is.ownis the page as a page of its own in the portal's menu, after Tickets, at the portal's link plus its style name.own:<id>is the page of its own of page<id>. A portal that does not draw the page shows nothing. positionstring- The top or the bottom of that page. Always
topforown. Onticketsit can also bebefore_issue, just above the portal's Issue tickets heading. - one of
topbefore_issuebottom every_portalboolean- Every portal of the event, including portals made later, less
hidden_from_portal_ids. Otherwise it shows inportal_idsonly.
Responses
201The placement.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The placement cannot be saved. The errorcodeis the reason:portal_page(not tickets, details, documents or downloads),portal_position(not top, bottom or before_issue),portal_before_issue(before_issue on a page other than tickets),portal_none(no portals named and not every portal),portal_unknown(a portal this event does not have), orportal_ids_with_every_portal/hidden_without_every_portal(a portal list for the other mode). A piece already in that place in a portal is not added there twice.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/pages/1/portal-placements \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"page":"tickets"}'
// 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/pages/1/portal-placements", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"page": "tickets"
}),
});
const data = await res.json();
{
"id": 4,
"page": "tickets",
"position": "top",
"every_portal": false,
"portal_ids": [],
"hidden_from_portal_ids": []
}
Take a Content page out of portals
DELETE /events/{slug}/pages/{id}/portal-placements/{placement}
Removes this one place. The piece stays in Content and anywhere else it shows.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The page's id, as
GET /events/{slug}/pageslists it. placementinteger required- The placement's id, as a page's
portal_placementslists it.
Response fields
deletedboolean
Responses
200Removed.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/pages/1/portal-placements/{placement} \
-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/pages/1/portal-placements/{placement}", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"deleted": true
}
Move a Content page in portals
PATCH /events/{slug}/pages/{id}/portal-placements/{placement}
Only the keys present change. Moving it to another page or position puts it last there. Turning every_portal on or off starts its portal lists afresh from the body.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The page's id, as
GET /events/{slug}/pageslists it. placementinteger required- The placement's id, as a page's
portal_placementslists it.
Request body
pagestring- The portal page:
tickets,details,documentsordownloads.ownmakes it a page of its own in the portal's menu, which needs the page to have a title.own:<id>is the top or bottom of another page's own page. positionstring- Top or bottom of the page. Not needed for
own.before_issueis just above the Issue tickets heading, onticketsonly. - one of
topbefore_issuebottom
Response fields
idintegerpagestring- The portal page it sits on.
ticketsis the portal's own link, where the ticket form is.ownis the page as a page of its own in the portal's menu, after Tickets, at the portal's link plus its style name.own:<id>is the page of its own of page<id>. A portal that does not draw the page shows nothing. positionstring- The top or the bottom of that page. Always
topforown. Onticketsit can also bebefore_issue, just above the portal's Issue tickets heading. - one of
topbefore_issuebottom every_portalboolean- Every portal of the event, including portals made later, less
hidden_from_portal_ids. Otherwise it shows inportal_idsonly.
Responses
200The placement as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The placement cannot be saved. The errorcodeis the reason:portal_page(not tickets, details, documents or downloads),portal_position(not top, bottom or before_issue),portal_before_issue(before_issue on a page other than tickets),portal_none(no portals named and not every portal),portal_unknown(a portal this event does not have), orportal_ids_with_every_portal/hidden_without_every_portal(a portal list for the other mode). A piece already in that place in a portal is not added there twice.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/pages/1/portal-placements/{placement} \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"page":"tickets"}'
// 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/pages/1/portal-placements/{placement}", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"page": "tickets"
}),
});
const data = await res.json();
{
"id": 4,
"page": "tickets",
"position": "top",
"every_portal": false,
"portal_ids": [],
"hidden_from_portal_ids": []
}
List an event's public addresses
GET /events/{slug}/public-pages
Every address this event answers on — its own page first, then the audience pages, portals, application pages and widgets. A portal is listed only while its shareable registration link is on and can give at least one ticket for nothing, the same rule as the portal's share_url; any other portal has no public address. A draft event has no public addresses at all, which draft says.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
public_pagesarray of PublicPagepublic_pages[].kindstring- event (the event's own page), audience, portal, application or embed.
- one of
eventaudienceportalapplicationembed public_pages[].idinteger- The row within its kind; 0 for the event's own page, the one row that is not a record of its own.
draftboolean
Responses
200The addresses.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/public-pages \
-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/public-pages", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"public_pages": [
{
"kind": "audience",
"id": 6,
"name": "Press",
"address": "/annual/press",
"open": true
}
],
"draft": false
}
Close a public address
POST /events/{slug}/public-pages/{kind}/{id}/close
Stops the address answering; the link keeps working and says so. Closing stamps a time, so "when did this stop selling?" has an answer — a flip that changes nothing writes nothing rather than re-stamping it. The row is looked up in the event's own inventory, so a kind this event has none of and another event's id are the same 404. A portal is in the inventory only while it is listed (see listPublicPages); revoke or reinstate any other portal through its own portal endpoints.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
kindstring required- The address's kind, as
GET /events/{slug}/public-pagesreports it. idinteger required- The row within its kind; 0 for the event's own page.
Response fields
kindstring- event (the event's own page), audience, portal, application or embed.
- one of
eventaudienceportalapplicationembed idinteger- The row within its kind; 0 for the event's own page, the one row that is not a record of its own.
Responses
200The address as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/public-pages/{kind}/1/close \
-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/public-pages/{kind}/1/close", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"kind": "audience",
"id": 6,
"name": "Press",
"address": "/annual/press",
"open": true
}
Open a public address
POST /events/{slug}/public-pages/{kind}/{id}/open
Starts the address answering again. The row is looked up in the event's own inventory, so a kind this event has none of and another event's id are the same 404. A portal is in the inventory only while it is listed (see listPublicPages); revoke or reinstate any other portal through its own portal endpoints.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
kindstring required- The address's kind, as
GET /events/{slug}/public-pagesreports it. idinteger required- The row within its kind; 0 for the event's own page.
Response fields
kindstring- event (the event's own page), audience, portal, application or embed.
- one of
eventaudienceportalapplicationembed idinteger- The row within its kind; 0 for the event's own page, the one row that is not a record of its own.
Responses
200The address as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/public-pages/{kind}/1/open \
-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/public-pages/{kind}/1/open", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"kind": "audience",
"id": 6,
"name": "Press",
"address": "/annual/press",
"open": true
}
Discounts
List coupons
GET /events/{slug}/coupons
A coupon is the discount RULE — a percentage (percent_bps, basis points) or a fixed amount (amount_off_cents, in the event's currency, with amounts_off per extra currency) — off each eligible ticket (applicability: ticket) or off the order once (order), for some ticket types (ticket_type_ids) or all (empty), within an optional window (starts_at/ends_at, RFC 3339; the end is exclusive). Promo codes are its redeemable codes. A coupon with no codes is a discount nobody can claim. Live coupons, newest first, each with how many live codes it has.
Requires coupons.view
Path parameters
slugstring required- The event's slug.
Response fields
couponsarray of Couponcoupons[].amount_off_centsinteger- The fixed amount in the event's currency. 0 on a percent coupon.
coupons[].amounts_offobject- Fixed amount per currency, lowercase code → cents. Empty on a percent coupon. On a write, the event's own currency must equal
amount_off_cents;nullclears a currency. coupons[].applicabilitystringticket: off each eligible ticket.order: off the order once.- one of
ticketorder coupons[].roundingstring- How a percentage settles the part of a cent it comes to, on each ticket.
downunless the coupon came from Tito Pro (half_up) or Tito Classic (half_down), or was edited from one, so it takes off what it took off there. Read-only. - one of
downhalf_uphalf_down coupons[].limit_unitstring- What one use of the coupon's limited codes is:
orders, ortickets(a coupon applied to each ticket only). Withtickets, a code with 10 uses discounts 10 tickets over any number of orders, and the order that crosses the limit gets the discount on as many tickets as are left. It can't change once a code has been used. - one of
orderstickets coupons[].combinableboolean- Whether the coupon's codes can share an order with any other code.
false: a code of it can only be the one code on an order, and a buyer adding a second code is refused. It isn't part of the price, so it can change at any time and rules checkouts from then on. coupons[].applies_on_volume_pricesboolean- Whether the coupon also comes off a ticket the order has taken enough of for one of its volume prices.
false: those tickets are charged the volume price with nothing off, and the order's other tickets keep the discount. It is part of the price and rules checkouts from then on: an order already taken keeps what it was charged. coupons[].shows_ticket_type_idsarray of integer- Ticket types kept off the event page that a buyer who applies one of the coupon's codes sees there too, in their usual place. Empty shows none.
coupons[].shows_after_used_upstring- What the shown ticket types do once a code's uses run out:
hidethem again, orkeepshowing them at full price. - one of
hidekeep coupons[].shows_only_its_ticketsboolean- Whether a buyer who applies one of the coupon's codes sees only the ticket types in
ticket_type_ids, with the event page's other tickets hidden. Always false whenticket_type_idsis empty. coupons[].min_per_ticket_typeinteger- How many of one ticket type an order needs before the coupon comes off that type. Each type is counted on its own. 0 is no minimum.
coupons[].max_per_ticket_typeinteger- How many of each ticket type in an order the coupon comes off. Any more of that type are full price. 0 is no maximum.
coupons[].created_atstring (RFC 3339) or null
Responses
200Coupons.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/coupons \
-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/coupons", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"coupons": [
{
"id": 4,
"name": "Early bird",
"kind": "percent",
"percent_bps": 2500,
"amount_off_cents": 0,
"currency": "eur",
"amounts_off": {
"eur": 500,
"gbp": 400
},
"applicability": "ticket",
"rounding": "down",
"limit_unit": "orders",
"combinable": true,
"applies_on_volume_prices": true,
"shows_ticket_type_ids": [
9
],
"shows_after_used_up": "hide",
"shows_only_its_tickets": false,
"min_per_ticket_type": 0,
"max_per_ticket_type": 0,
"ticket_type_ids": [
7
],
"starts_at": "2027-01-01T00:00:00Z",
"ends_at": null,
"created_at": "2026-09-07T18:20:00Z",
"codes": 3
}
]
}
Create a coupon
POST /events/{slug}/coupons
The rule, as the "New coupon" form makes it. Give exactly one of percent_bps and amount_off_cents. amounts_off sets a fixed coupon's amount in each extra currency the event sells in; a currency left out is one the coupon does not apply in. Then POST …/codes to mint the codes buyers type.
Requires coupons.manage
Path parameters
slugstring required- The event's slug.
Request body
amounts_offobject- Fixed amount per currency, lowercase code → cents. Empty on a percent coupon. On a write, the event's own currency must equal
amount_off_cents;nullclears a currency. limit_unitstring- What one use of the coupon's limited codes is:
orders, ortickets(a coupon applied to each ticket only). Withtickets, a code with 10 uses discounts 10 tickets over any number of orders, and the order that crosses the limit gets the discount on as many tickets as are left. It can't change once a code has been used. - one of
orderstickets combinableboolean- Default
true.falselets a code of this coupon be only the one code on an order. Imported Tito Classic coupons startfalse. applies_on_volume_pricesboolean- Default
true.falsetakes nothing off a ticket the order has taken enough of for one of its volume prices. Absent on a PATCH leaves it as it is. shows_ticket_type_idsarray of integer- Ticket types of this event, kept off its event page, that a buyer who applies one of the coupon's codes sees there too. Empty shows none; absent leaves them as they are.
shows_after_used_upstring- Default
hide. What the shown ticket types do once a code's uses run out:hidethem again, orkeepshowing them at full price. - one of
hidekeep shows_only_its_ticketsboolean- Default
false. Whether a buyer who applies one of the coupon's codes sees only the ticket types inticket_type_ids, with the event page's other tickets hidden. Needsticket_type_ids. Left out, it stays as it is. min_per_ticket_typeinteger- Default
0, no minimum. How many of one ticket type an order needs before the coupon comes off that type, 1 to 1000000. For a coupon applied to each ticket only. Left out, it stays as it is. max_per_ticket_typeinteger- Default
0, no maximum. How many of each ticket type in an order the coupon comes off, 1 to 1000000. It may be belowmin_per_ticket_type: a minimum of 5 and a maximum of 2 discounts 2 of an order's 5 or more. For a coupon applied to each ticket only. Left out, it stays as it is.
Response fields
amounts_offobject- Fixed amount per currency, lowercase code → cents. Empty on a percent coupon. On a write, the event's own currency must equal
amount_off_cents;nullclears a currency. applicabilitystringticket: off each eligible ticket.order: off the order once.- one of
ticketorder roundingstring- How a percentage settles the part of a cent it comes to, on each ticket.
downunless the coupon came from Tito Pro (half_up) or Tito Classic (half_down), or was edited from one, so it takes off what it took off there. Read-only. - one of
downhalf_uphalf_down limit_unitstring- What one use of the coupon's limited codes is:
orders, ortickets(a coupon applied to each ticket only). Withtickets, a code with 10 uses discounts 10 tickets over any number of orders, and the order that crosses the limit gets the discount on as many tickets as are left. It can't change once a code has been used. - one of
orderstickets combinableboolean- Whether the coupon's codes can share an order with any other code.
false: a code of it can only be the one code on an order, and a buyer adding a second code is refused. It isn't part of the price, so it can change at any time and rules checkouts from then on. applies_on_volume_pricesboolean- Whether the coupon also comes off a ticket the order has taken enough of for one of its volume prices.
false: those tickets are charged the volume price with nothing off, and the order's other tickets keep the discount. It is part of the price and rules checkouts from then on: an order already taken keeps what it was charged. shows_ticket_type_idsarray of integer- Ticket types kept off the event page that a buyer who applies one of the coupon's codes sees there too, in their usual place. Empty shows none.
shows_after_used_upstring- What the shown ticket types do once a code's uses run out:
hidethem again, orkeepshowing them at full price. - one of
hidekeep shows_only_its_ticketsboolean- Whether a buyer who applies one of the coupon's codes sees only the ticket types in
ticket_type_ids, with the event page's other tickets hidden. Always false whenticket_type_idsis empty. min_per_ticket_typeinteger- How many of one ticket type an order needs before the coupon comes off that type. Each type is counted on its own. 0 is no minimum.
max_per_ticket_typeinteger- How many of each ticket type in an order the coupon comes off. Any more of that type are full price. 0 is no maximum.
created_atstring (RFC 3339) or null
Responses
201The new coupon.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body was unreadable or carried an unknown field; both or neither amount given, or one out of bounds; an unknownapplicability; a ticket type not on this event;amounts_offon a percent coupon or naming a currency the event does not sell in (invalid_body); a malformed date or an end before the start (invalid_dates).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/coupons \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Early bird"}'
// 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/coupons", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Early bird"
}),
});
const data = await res.json();
{
"id": 4,
"name": "Early bird",
"kind": "percent",
"percent_bps": 2500,
"amount_off_cents": 0,
"currency": "eur",
"amounts_off": {
"eur": 500,
"gbp": 400
},
"applicability": "ticket",
"rounding": "down",
"limit_unit": "orders",
"combinable": true,
"applies_on_volume_prices": true,
"shows_ticket_type_ids": [
9
],
"shows_after_used_up": "hide",
"shows_only_its_tickets": false,
"min_per_ticket_type": 0,
"max_per_ticket_type": 0,
"ticket_type_ids": [
7
],
"starts_at": "2027-01-01T00:00:00Z",
"ends_at": null,
"created_at": "2026-09-07T18:20:00Z",
"codes": 3
}
Delete a coupon
DELETE /events/{slug}/coupons/{id}
Retires the rule and every live code it has. Orders that used it keep their discount rows and their history; nothing is destroyed.
Requires coupons.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The coupon's id, as
GET /events/{slug}/couponslists it.
Response fields
Responses
200Deleted.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/coupons/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/coupons/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 4,
"deleted": true
}
Get a coupon
GET /events/{slug}/coupons/{id}
One live coupon.
Requires coupons.view
Path parameters
slugstring required- The event's slug.
idinteger required- The coupon's id, as
GET /events/{slug}/couponslists it.
Response fields
amounts_offobject- Fixed amount per currency, lowercase code → cents. Empty on a percent coupon. On a write, the event's own currency must equal
amount_off_cents;nullclears a currency. applicabilitystringticket: off each eligible ticket.order: off the order once.- one of
ticketorder roundingstring- How a percentage settles the part of a cent it comes to, on each ticket.
downunless the coupon came from Tito Pro (half_up) or Tito Classic (half_down), or was edited from one, so it takes off what it took off there. Read-only. - one of
downhalf_uphalf_down limit_unitstring- What one use of the coupon's limited codes is:
orders, ortickets(a coupon applied to each ticket only). Withtickets, a code with 10 uses discounts 10 tickets over any number of orders, and the order that crosses the limit gets the discount on as many tickets as are left. It can't change once a code has been used. - one of
orderstickets combinableboolean- Whether the coupon's codes can share an order with any other code.
false: a code of it can only be the one code on an order, and a buyer adding a second code is refused. It isn't part of the price, so it can change at any time and rules checkouts from then on. applies_on_volume_pricesboolean- Whether the coupon also comes off a ticket the order has taken enough of for one of its volume prices.
false: those tickets are charged the volume price with nothing off, and the order's other tickets keep the discount. It is part of the price and rules checkouts from then on: an order already taken keeps what it was charged. shows_ticket_type_idsarray of integer- Ticket types kept off the event page that a buyer who applies one of the coupon's codes sees there too, in their usual place. Empty shows none.
shows_after_used_upstring- What the shown ticket types do once a code's uses run out:
hidethem again, orkeepshowing them at full price. - one of
hidekeep shows_only_its_ticketsboolean- Whether a buyer who applies one of the coupon's codes sees only the ticket types in
ticket_type_ids, with the event page's other tickets hidden. Always false whenticket_type_idsis empty. min_per_ticket_typeinteger- How many of one ticket type an order needs before the coupon comes off that type. Each type is counted on its own. 0 is no minimum.
max_per_ticket_typeinteger- How many of each ticket type in an order the coupon comes off. Any more of that type are full price. 0 is no maximum.
created_atstring (RFC 3339) or null
Responses
200The coupon.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/coupons/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/coupons/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 4,
"name": "Early bird",
"kind": "percent",
"percent_bps": 2500,
"amount_off_cents": 0,
"currency": "eur",
"amounts_off": {
"eur": 500,
"gbp": 400
},
"applicability": "ticket",
"rounding": "down",
"limit_unit": "orders",
"combinable": true,
"applies_on_volume_prices": true,
"shows_ticket_type_ids": [
9
],
"shows_after_used_up": "hide",
"shows_only_its_tickets": false,
"min_per_ticket_type": 0,
"max_per_ticket_type": 0,
"ticket_type_ids": [
7
],
"starts_at": "2027-01-01T00:00:00Z",
"ends_at": null,
"created_at": "2026-09-07T18:20:00Z",
"codes": 3
}
Update a coupon
PATCH /events/{slug}/coupons/{id}
The rule's edit form. Absent fields are unchanged; an explicit null and an unknown field are refused. Sending percent_bps to a fixed coupon switches its kind and clears its currency amounts, as the form's radio does; ticket_type_ids replaces the whole list; "" clears a date.
Requires coupons.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The coupon's id, as
GET /events/{slug}/couponslists it.
Request body
amounts_offobject- Fixed amount per currency, lowercase code → cents. Empty on a percent coupon. On a write, the event's own currency must equal
amount_off_cents;nullclears a currency. limit_unitstring- What one use of the coupon's limited codes is:
orders, ortickets(a coupon applied to each ticket only). Withtickets, a code with 10 uses discounts 10 tickets over any number of orders, and the order that crosses the limit gets the discount on as many tickets as are left. It can't change once a code has been used. - one of
orderstickets combinableboolean- Default
true.falselets a code of this coupon be only the one code on an order. Imported Tito Classic coupons startfalse. applies_on_volume_pricesboolean- Default
true.falsetakes nothing off a ticket the order has taken enough of for one of its volume prices. Absent on a PATCH leaves it as it is. shows_ticket_type_idsarray of integer- Ticket types of this event, kept off its event page, that a buyer who applies one of the coupon's codes sees there too. Empty shows none; absent leaves them as they are.
shows_after_used_upstring- Default
hide. What the shown ticket types do once a code's uses run out:hidethem again, orkeepshowing them at full price. - one of
hidekeep shows_only_its_ticketsboolean- Default
false. Whether a buyer who applies one of the coupon's codes sees only the ticket types inticket_type_ids, with the event page's other tickets hidden. Needsticket_type_ids. Left out, it stays as it is. min_per_ticket_typeinteger- Default
0, no minimum. How many of one ticket type an order needs before the coupon comes off that type, 1 to 1000000. For a coupon applied to each ticket only. Left out, it stays as it is. max_per_ticket_typeinteger- Default
0, no maximum. How many of each ticket type in an order the coupon comes off, 1 to 1000000. It may be belowmin_per_ticket_type: a minimum of 5 and a maximum of 2 discounts 2 of an order's 5 or more. For a coupon applied to each ticket only. Left out, it stays as it is.
Response fields
amounts_offobject- Fixed amount per currency, lowercase code → cents. Empty on a percent coupon. On a write, the event's own currency must equal
amount_off_cents;nullclears a currency. applicabilitystringticket: off each eligible ticket.order: off the order once.- one of
ticketorder roundingstring- How a percentage settles the part of a cent it comes to, on each ticket.
downunless the coupon came from Tito Pro (half_up) or Tito Classic (half_down), or was edited from one, so it takes off what it took off there. Read-only. - one of
downhalf_uphalf_down limit_unitstring- What one use of the coupon's limited codes is:
orders, ortickets(a coupon applied to each ticket only). Withtickets, a code with 10 uses discounts 10 tickets over any number of orders, and the order that crosses the limit gets the discount on as many tickets as are left. It can't change once a code has been used. - one of
orderstickets combinableboolean- Whether the coupon's codes can share an order with any other code.
false: a code of it can only be the one code on an order, and a buyer adding a second code is refused. It isn't part of the price, so it can change at any time and rules checkouts from then on. applies_on_volume_pricesboolean- Whether the coupon also comes off a ticket the order has taken enough of for one of its volume prices.
false: those tickets are charged the volume price with nothing off, and the order's other tickets keep the discount. It is part of the price and rules checkouts from then on: an order already taken keeps what it was charged. shows_ticket_type_idsarray of integer- Ticket types kept off the event page that a buyer who applies one of the coupon's codes sees there too, in their usual place. Empty shows none.
shows_after_used_upstring- What the shown ticket types do once a code's uses run out:
hidethem again, orkeepshowing them at full price. - one of
hidekeep shows_only_its_ticketsboolean- Whether a buyer who applies one of the coupon's codes sees only the ticket types in
ticket_type_ids, with the event page's other tickets hidden. Always false whenticket_type_idsis empty. min_per_ticket_typeinteger- How many of one ticket type an order needs before the coupon comes off that type. Each type is counted on its own. 0 is no minimum.
max_per_ticket_typeinteger- How many of each ticket type in an order the coupon comes off. Any more of that type are full price. 0 is no maximum.
created_atstring (RFC 3339) or null
Responses
200The coupon as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422As for create (invalid_body,invalid_dates).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/coupons/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Early bird"}'
// 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/coupons/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Early bird"
}),
});
const data = await res.json();
{
"id": 4,
"name": "Early bird",
"kind": "percent",
"percent_bps": 2500,
"amount_off_cents": 0,
"currency": "eur",
"amounts_off": {
"eur": 500,
"gbp": 400
},
"applicability": "ticket",
"rounding": "down",
"limit_unit": "orders",
"combinable": true,
"applies_on_volume_prices": true,
"shows_ticket_type_ids": [
9
],
"shows_after_used_up": "hide",
"shows_only_its_tickets": false,
"min_per_ticket_type": 0,
"max_per_ticket_type": 0,
"ticket_type_ids": [
7
],
"starts_at": "2027-01-01T00:00:00Z",
"ends_at": null,
"created_at": "2026-09-07T18:20:00Z",
"codes": 3
}
Remove a coupon-code batch
DELETE /events/{slug}/coupons/{id}/batches/{batchID}
Deletes never-used codes and retires codes with redemption history in one transaction. An existing empty batch returns 404.
Requires coupons.manage
Path parameters
slugstring required- The event's slug.
idinteger requiredbatchIDinteger required
Response fields
Responses
200Operation completed.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/coupons/1/batches/{batchID} \
-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/coupons/1/batches/{batchID}", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"deleted": 1,
"retired": 1
}
List a coupon's codes
GET /events/{slug}/coupons/{id}/codes
Every code the coupon has minted, retired ones last and marked, each with how many pending or paid orders hold it.
Requires coupons.view
Path parameters
slugstring required- The event's slug.
idinteger required- The coupon's id, as
GET /events/{slug}/couponslists it.
Response fields
codesarray of PromoCodecodes[].redemptionsinteger- Pending and paid orders holding it, or the tickets they took the discount on when the coupon's
limit_unitistickets. A canceled order, or one refunded in full, gives its uses back. codes[].created_atstring (RFC 3339) or nullcodes[].retiredboolean- Removed after being redeemed: no longer accepted, kept for its orders' history.
Responses
200Codes.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/coupons/1/codes \
-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/coupons/1/codes", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"codes": [
{
"id": 9,
"coupon_id": 4,
"code": "SPRING25",
"max_redemptions": 100,
"redemptions": 12,
"label": "Newsletter",
"batch_id": 0,
"created_at": "2026-09-07T18:21:00Z",
"retired": false
}
]
}
Add a promo code
POST /events/{slug}/coupons/{id}/codes
One code buyers type at checkout. Codes are case-sensitive, 1–191 characters with no whitespace and none of / & % # , ; ", unique per event among live codes. max_redemptions 0 (the default) is unlimited; a refund gives a redemption back. label names whose code it is.
Requires coupons.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The coupon's id, as
GET /events/{slug}/couponslists it.
Request body
codestring required- 1–191 characters with no whitespace and none of
/ & % # , ; "; case-sensitive; unique per event among live codes.
Response fields
redemptionsinteger- Pending and paid orders holding it, or the tickets they took the discount on when the coupon's
limit_unitistickets. A canceled order, or one refunded in full, gives its uses back. created_atstring (RFC 3339) or null
Responses
201The new code.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409A live code with that spelling already exists on this event (code_taken).422The body was unreadable or carried an unknown field, the code is malformed,max_redemptionsis negative, or the label is too long (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/coupons/1/codes \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"code":"SPRING25"}'
// 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/coupons/1/codes", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"code": "SPRING25"
}),
});
const data = await res.json();
{
"id": 9,
"coupon_id": 4,
"code": "SPRING25",
"max_redemptions": 100,
"redemptions": 12,
"label": "Newsletter",
"batch_id": 0,
"created_at": "2026-09-07T18:21:00Z",
"retired": false
}
Generate a batch of codes
POST /events/{slug}/coupons/{id}/codes/batch
Up to the account's batch limit (500 unless Tito has raised it) of generated codes in one batch — the shape a sponsor or a mail-merge gets — each prefix plus random characters, all with the same cap and label. Answers the batch id and its codes; GET …/codes lists them by batch_id afterwards.
Requires coupons.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The coupon's id, as
GET /events/{slug}/couponslists it.
Request body
countinteger required- How many codes, from 1 to the account's batch limit (500 unless Tito has raised it).
Response fields
batch_idintegercodesarray of PromoCodecodes[].redemptionsinteger- Pending and paid orders holding it, or the tickets they took the discount on when the coupon's
limit_unitistickets. A canceled order, or one refunded in full, gives its uses back. codes[].created_atstring (RFC 3339) or nullcodes[].retiredboolean- Removed after being redeemed: no longer accepted, kept for its orders' history.
Responses
201The batch.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409A generated code collided with a live one; try again (code_taken).422countoutside 1 to the account's batch limit (500 unless Tito has raised it), a malformedprefix, a negativemax_redemptions, or a label too long (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/coupons/1/codes/batch \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"count":50}'
// 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/coupons/1/codes/batch", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"count": 50
}),
});
const data = await res.json();
{
"batch_id": 2,
"codes": [
{
"id": 9,
"coupon_id": 4,
"code": "SPRING25",
"max_redemptions": 100,
"redemptions": 12,
"label": "Newsletter",
"batch_id": 0,
"created_at": "2026-09-07T18:21:00Z",
"retired": false
}
]
}
Import coupon codes
POST /events/{slug}/coupons/{id}/codes/import
Imports a CSV or pasted code list using the organizer import parser. At most the account's batch limit of codes (500 unless Tito has raised it) and 1 MiB of decoded text, bounded by an 8 MiB request body. Columns are code, optional maximum redemptions, and optional label. A nonnumeric second column is the label. A header is optional. Default max_redemptions is 0 (unlimited). No per-row discount-rule columns. Any invalid row leaves the batch unwritten.
Requires coupons.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Request body
Response fields
batch_idintegercodesarray of PromoCodecodes[].redemptionsinteger- Pending and paid orders holding it, or the tickets they took the discount on when the coupon's
limit_unitistickets. A canceled order, or one refunded in full, gives its uses back. codes[].created_atstring (RFC 3339) or nullcodes[].retiredboolean- Removed after being redeemed: no longer accepted, kept for its orders' history.
Responses
201Operation completed.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Code collided with a live code during insertion.413Too many codes or too much input.422Invalid body or code rows. Row errors include line, code and reason.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/coupons/1/codes/import \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"codes":"EARLYBIRD"}'
// 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/coupons/1/codes/import", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"codes": "EARLYBIRD"
}),
});
const data = await res.json();
{
"batch_id": 2,
"codes": [
{
"id": 9,
"coupon_id": 4,
"code": "SPRING25",
"max_redemptions": 100,
"redemptions": 12,
"label": "Newsletter",
"batch_id": 0,
"created_at": "2026-09-07T18:21:00Z",
"retired": false
}
]
}
Remove a promo code
DELETE /events/{slug}/coupons/{id}/codes/{cid}
The leaked-code control: a code nobody ever used is DELETED, one that was redeemed is RETIRED so its orders keep reading. The answer says which. A retired code still appears in GET …/codes, marked.
Requires coupons.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The coupon's id, as
GET /events/{slug}/couponslists it. cidinteger required- The promo code's id, as
GET …/codeslists it.
Response fields
Responses
200What happened to it.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/coupons/1/codes/{cid} \
-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/coupons/1/codes/{cid}", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 9,
"deleted": true,
"retired": false
}
Relabel a promo code
PATCH /events/{slug}/coupons/{id}/codes/{cid}
The label is the one thing about a code that can change after it is minted; the code and its cap are what was handed out.
Requires coupons.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The coupon's id, as
GET /events/{slug}/couponslists it. cidinteger required- The promo code's id, as
GET …/codeslists it.
Request body
Response fields
redemptionsinteger- Pending and paid orders holding it, or the tickets they took the discount on when the coupon's
limit_unitistickets. A canceled order, or one refunded in full, gives its uses back. created_atstring (RFC 3339) or null
Responses
200The code.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 anything butlabel, or the label is too long (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/coupons/1/codes/{cid} \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"label":"Print run"}'
// 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/coupons/1/codes/{cid}", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"label": "Print run"
}),
});
const data = await res.json();
{
"id": 9,
"coupon_id": 4,
"code": "SPRING25",
"max_redemptions": 100,
"redemptions": 12,
"label": "Newsletter",
"batch_id": 0,
"created_at": "2026-09-07T18:21:00Z",
"retired": false
}
Duplicate a coupon
POST /events/{slug}/coupons/{id}/duplicate
Copies the rule and live codes, with fresh unique codes, reset redemption history and an unbounded time window. No request body.
Requires coupons.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Response fields
amounts_offobject- Fixed amount per currency, lowercase code → cents. Empty on a percent coupon. On a write, the event's own currency must equal
amount_off_cents;nullclears a currency. applicabilitystringticket: off each eligible ticket.order: off the order once.- one of
ticketorder roundingstring- How a percentage settles the part of a cent it comes to, on each ticket.
downunless the coupon came from Tito Pro (half_up) or Tito Classic (half_down), or was edited from one, so it takes off what it took off there. Read-only. - one of
downhalf_uphalf_down limit_unitstring- What one use of the coupon's limited codes is:
orders, ortickets(a coupon applied to each ticket only). Withtickets, a code with 10 uses discounts 10 tickets over any number of orders, and the order that crosses the limit gets the discount on as many tickets as are left. It can't change once a code has been used. - one of
orderstickets combinableboolean- Whether the coupon's codes can share an order with any other code.
false: a code of it can only be the one code on an order, and a buyer adding a second code is refused. It isn't part of the price, so it can change at any time and rules checkouts from then on. applies_on_volume_pricesboolean- Whether the coupon also comes off a ticket the order has taken enough of for one of its volume prices.
false: those tickets are charged the volume price with nothing off, and the order's other tickets keep the discount. It is part of the price and rules checkouts from then on: an order already taken keeps what it was charged. shows_ticket_type_idsarray of integer- Ticket types kept off the event page that a buyer who applies one of the coupon's codes sees there too, in their usual place. Empty shows none.
shows_after_used_upstring- What the shown ticket types do once a code's uses run out:
hidethem again, orkeepshowing them at full price. - one of
hidekeep shows_only_its_ticketsboolean- Whether a buyer who applies one of the coupon's codes sees only the ticket types in
ticket_type_ids, with the event page's other tickets hidden. Always false whenticket_type_idsis empty. min_per_ticket_typeinteger- How many of one ticket type an order needs before the coupon comes off that type. Each type is counted on its own. 0 is no minimum.
max_per_ticket_typeinteger- How many of each ticket type in an order the coupon comes off. Any more of that type are full price. 0 is no maximum.
created_atstring (RFC 3339) or null
Responses
201Operation completed.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/coupons/1/duplicate \
-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/coupons/1/duplicate", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 4,
"name": "Early bird",
"kind": "percent",
"percent_bps": 2500,
"amount_off_cents": 0,
"currency": "eur",
"amounts_off": {
"eur": 500,
"gbp": 400
},
"applicability": "ticket",
"rounding": "down",
"limit_unit": "orders",
"combinable": true,
"applies_on_volume_prices": true,
"shows_ticket_type_ids": [
9
],
"shows_after_used_up": "hide",
"shows_only_its_tickets": false,
"min_per_ticket_type": 0,
"max_per_ticket_type": 0,
"ticket_type_ids": [
7
],
"starts_at": "2027-01-01T00:00:00Z",
"ends_at": null,
"created_at": "2026-09-07T18:20:00Z",
"codes": 3
}
Messages
List messages
GET /events/{slug}/messages
A message is an email from the organizer to the people of an event: a subject and a body, an audience (all ticket holders, buyers, both, invited, waitlist, signups, or type:<ticket type id>), an optional segment narrowing it, and a look. It is a draft until sent; send puts it on a sixty-second hold (sending) that stop can still catch, or on a schedule (scheduled) up to six months out; once it goes it is sent and stays as the record of what went out. Recipients are resolved again at the moment it goes. Newest first, each with its delivery counts. API message records include a kind value of bulk or individual, and message lists can filter by kind. The event outbox endpoint backs a sent message's Recipients page in the admin: it accepts message_id, status, q, and page, and returns saved email records scoped to that event.
Requires messages.view
Path parameters
slugstring required- The event's slug.
Query parameters
kindstring
Response fields
messagesarray of Messagemessages[].bodystring- The body as prose: what was typed, with the composer's variables read as their fallbacks and its blocks and marks dropped.
messages[].body_sourcestring- The stored source as the composer keeps it, variables, blocks and marks included. Read-only.
messages[].previewstring- The line an inbox shows after the subject: plain words on one line, hidden in the email itself. Empty for none.
messages[].audiencestring- Who it goes to:
all,buyers,both,invited,waitlist,signups, ortype:<ticket type id>.waitlistis everyone on the event's waitlists except those who left, and each copy ends with that person's own link to leave.signupsis everyone who confirmed a sign-up on one of the event's widgets that collect emails, never anyone who withdrew; it takes no segment, and each copy ends with that person's own link to stop updates. messages[].segmentobject or null- The compose page's own filters, as it stores them (checked_in, confirmed, since, type, ostatus, …); null for none.
confirmedisyesorno: whether the attendee has said they are still coming. A since date begins at midnight in the event's timezone.pickedis a hand-picked set of ticket references, comma-separated, as the attendee list sends ticked rows. It narrows the ticket door to those tickets and the order door to whoever booked them, and unlike every other filter the set does not grow, so a scheduled message goes to the same people however the event changes. It must be a string of at most 2000 references, or the request is refused with 422invalid_body. Apickedvalue with no readable reference in it matches nobody. messages[].lookstring- How it is dressed: "" for the event's own look,
plain, ortext. - one of
plaintext messages[].calendarboolean- Whether the message carries a calendar file for the event. Each recipient's copy is built from their own order, so somebody holding a timed ticket gets their session as well as the event.
messages[].filesarray of MessageFile- Files attached to the message, in order. Read only: attaching one is a browser upload.
messages[].files[].filenamestringmessages[].files[].deliverystringemailwhen the file is sent with the message,linkwhen the email carries a download link to it instead,offlinewhen Tito took the file offline and it is sent neither way. Decided by the weight of everything attached, in order. An offline file adds no weight.- one of
emaillinkoffline messages[].statusstringdraft,scheduled,sending(on its hold, or going out),sent, orstopped(a sweep declined; seestop_reason).- one of
draftscheduledsendingsentstopped messages[].scheduled_forstring (RFC 3339) or null- The moment a person named; null for now or none.
messages[].send_afterstring (RFC 3339) or null- The fuse: when it goes; null when not on the clock.
messages[].deliveredinteger- Sent emails the email provider reports as delivered to the recipient's mail server.
messages[].waitinginteger- Sent emails the provider hasn't reported on yet, usually for seconds. One it is still trying (a full mailbox, a slow server) waits up to a day before it counts as bounced.
messages[].bouncedinteger- Sent emails the recipient's mail server turned away: the address doesn't exist, or a full mailbox stayed full for a day.
messages[].not_sentinteger- Emails the provider didn't send, because an earlier email to the address bounced or was reported as spam.
messages[].marked_as_spaminteger- Sent emails the recipient reported as spam. Whatever of
sentthese five leave was sent through a mail transport that can't report back (SMTP, or a sender imported from Tito Classic or Pro). messages[].stop_reasonstring- Why a sweep declined to send:
nobody,lateorarchived; "" otherwise. messages[].created_atstring (RFC 3339) or nullmessages[].updated_atstring (RFC 3339) or nullmessages[].email_sender_address_idintegeremail_sender_address_idis the ID of the selected address, or0for a domain choice or a message that follows the event. When it is0, a positiveemail_sender_ididentifies the chosen domain;email_sender_id: 0means follow the event.messages[].email_sender_idinteger- The sender chosen for this message (
GET /email-senders), 0 when it sends from its event's sender. This is the sender asked for: one that is not live when the message goes is replaced by the event's.
Responses
200Messages.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/messages \
-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/messages", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"messages": [
{
"id": 3,
"subject": "Doors open at 8",
"body": "See you there.\n\nBring ID.",
"body_source": "See you there.\n\nBring ID.",
"preview": "Bring your ticket and a jacket",
"audience": "all",
"segment": {
"checked_in": "no"
},
"look": "",
"calendar": false,
"files": [
{
"filename": "floor-plan.pdf",
"size": 284193,
"delivery": "email"
}
],
"status": "draft",
"scheduled_for": null,
"send_after": null,
"scheduled_count": 0,
"sent": 0,
"failed": 0,
"pending": 0,
"logged": 0,
"delivered": 401,
"waiting": 0,
"bounced": 6,
"not_sent": 3,
"marked_as_spam": 2,
"stop_reason": "",
"scheduled_by": "",
"stopped_by": "",
"created_at": "2026-09-07T20:50:00Z",
"updated_at": "2026-09-07T20:50:00Z",
"email_sender_address_id": 1,
"email_sender_id": 0,
"kind": "bulk"
}
]
}
Create a draft
POST /events/{slug}/messages
A draft, sent by nothing until POST …/{id}/send. audience defaults to all; look to the event's own. The API writes PROSE: what you send as body is stored as literal text through the composer's own writer, so figh syntax and mark syntax typed into it arrive as text and never as a directive or a mark. The composer's variables, blocks, marks and per-language variants are its own affordances; a message written there answers its prose reading in body and its full source in body_source.
Requires messages.manage
Path parameters
slugstring required- The event's slug.
Request body
previewstring- The line an inbox shows after the subject. Plain words; line breaks become spaces. Empty for none.
segmentobject or null- The compose page's own filters, as it stores them (checked_in, confirmed, since, type, ostatus, …); null for none.
confirmedisyesorno: whether the attendee has said they are still coming. A since date begins at midnight in the event's timezone.pickedis a hand-picked set of ticket references, comma-separated, as the attendee list sends ticked rows. It narrows the ticket door to those tickets and the order door to whoever booked them, and unlike every other filter the set does not grow, so a scheduled message goes to the same people however the event changes. It must be a string of at most 2000 references, or the request is refused with 422invalid_body. Apickedvalue with no readable reference in it matches nobody. A key the compose page has no filter for, or a value of the wrong type (typeis a ticket type's id as a number, not a string), is refused with 422invalid_bodynaming it, rather than stored as no narrowing and sent to the whole audience. email_sender_address_idintegeremail_sender_address_idis optional. A positive ID selects that exact address from theaddressesreturned byGET /email-senders. Its sender must be live; a saved message can keep its unchanged choice while the sender is pending. If both IDs are positive, the address must belong toemail_sender_id, or the request returns HTTP 422invalid_body. Address ID0follows the event unless a positiveemail_sender_idselects a domain. Omitting both fields from a messagePATCHpreserves its choice. A domain-onlyPATCHrepeating the current domain also preserves a selected address. To select the domain explicitly, send its ID with address ID0.email_sender_idinteger- The sender this message goes from: one of the account's live senders (
GET /email-senders), or 0 to send from the event's sender. Left out on a create, it is 0; left out on a PATCH, it is unchanged. Any other id is refused with422 invalid_body, except the message's own current choice.POST …/{id}/sendsends from what is stored.
Response fields
bodystring- The body as prose: what was typed, with the composer's variables read as their fallbacks and its blocks and marks dropped.
body_sourcestring- The stored source as the composer keeps it, variables, blocks and marks included. Read-only.
previewstring- The line an inbox shows after the subject: plain words on one line, hidden in the email itself. Empty for none.
audiencestring- Who it goes to:
all,buyers,both,invited,waitlist,signups, ortype:<ticket type id>.waitlistis everyone on the event's waitlists except those who left, and each copy ends with that person's own link to leave.signupsis everyone who confirmed a sign-up on one of the event's widgets that collect emails, never anyone who withdrew; it takes no segment, and each copy ends with that person's own link to stop updates. segmentobject or null- The compose page's own filters, as it stores them (checked_in, confirmed, since, type, ostatus, …); null for none.
confirmedisyesorno: whether the attendee has said they are still coming. A since date begins at midnight in the event's timezone.pickedis a hand-picked set of ticket references, comma-separated, as the attendee list sends ticked rows. It narrows the ticket door to those tickets and the order door to whoever booked them, and unlike every other filter the set does not grow, so a scheduled message goes to the same people however the event changes. It must be a string of at most 2000 references, or the request is refused with 422invalid_body. Apickedvalue with no readable reference in it matches nobody. calendarboolean- Whether the message carries a calendar file for the event. Each recipient's copy is built from their own order, so somebody holding a timed ticket gets their session as well as the event.
filesarray of MessageFile- Files attached to the message, in order. Read only: attaching one is a browser upload.
files[].filenamestringfiles[].deliverystringemailwhen the file is sent with the message,linkwhen the email carries a download link to it instead,offlinewhen Tito took the file offline and it is sent neither way. Decided by the weight of everything attached, in order. An offline file adds no weight.- one of
emaillinkoffline statusstringdraft,scheduled,sending(on its hold, or going out),sent, orstopped(a sweep declined; seestop_reason).- one of
draftscheduledsendingsentstopped deliveredinteger- Sent emails the email provider reports as delivered to the recipient's mail server.
waitinginteger- Sent emails the provider hasn't reported on yet, usually for seconds. One it is still trying (a full mailbox, a slow server) waits up to a day before it counts as bounced.
bouncedinteger- Sent emails the recipient's mail server turned away: the address doesn't exist, or a full mailbox stayed full for a day.
not_sentinteger- Emails the provider didn't send, because an earlier email to the address bounced or was reported as spam.
marked_as_spaminteger- Sent emails the recipient reported as spam. Whatever of
sentthese five leave was sent through a mail transport that can't report back (SMTP, or a sender imported from Tito Classic or Pro). created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or nullemail_sender_address_idintegeremail_sender_address_idis the ID of the selected address, or0for a domain choice or a message that follows the event. When it is0, a positiveemail_sender_ididentifies the chosen domain;email_sender_id: 0means follow the event.email_sender_idinteger- The sender chosen for this message (
GET /email-senders), 0 when it sends from its event's sender. This is the sender asked for: one that is not live when the message goes is replaced by the event's.
Responses
201The draft.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body was unreadable or carried an unknown field; both subject and body blank; an audience that does not resolve; a look outside "", plain, text; a segment that is not an object; anemail_sender_idthat is not one of the account's live senders (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/messages \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"subject":"Doors open at 8"}'
// 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/messages", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"subject": "Doors open at 8"
}),
});
const data = await res.json();
{
"id": 3,
"subject": "Doors open at 8",
"body": "See you there.\n\nBring ID.",
"body_source": "See you there.\n\nBring ID.",
"preview": "Bring your ticket and a jacket",
"audience": "all",
"segment": {
"checked_in": "no"
},
"look": "",
"calendar": false,
"files": [
{
"filename": "floor-plan.pdf",
"size": 284193,
"delivery": "email"
}
],
"status": "draft",
"scheduled_for": null,
"send_after": null,
"scheduled_count": 0,
"sent": 0,
"failed": 0,
"pending": 0,
"logged": 0,
"delivered": 401,
"waiting": 0,
"bounced": 6,
"not_sent": 3,
"marked_as_spam": 2,
"stop_reason": "",
"scheduled_by": "",
"stopped_by": "",
"created_at": "2026-09-07T20:50:00Z",
"updated_at": "2026-09-07T20:50:00Z",
"email_sender_address_id": 1,
"email_sender_id": 0,
"kind": "bulk"
}
Delete a draft
DELETE /events/{slug}/messages/{id}
A draft or a stopped message. One on the clock is 409 scheduled (stop it first); a sent one is 409 sent and stays, because its deliveries are the record of what went out.
Requires messages.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The message's id, as
GET /events/{slug}/messageslists it.
Response fields
Responses
200Deleted.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409On the clock (scheduled) or already gone (sent).500Internal error.
curl -X DELETE https://<yourslug>.go.tito.io/api/v1/events/annual-conf/messages/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/messages/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"deleted": true
}
Get a message
GET /events/{slug}/messages/{id}
One message with its delivery counts so far.
Requires messages.view
Path parameters
slugstring required- The event's slug.
idinteger required- The message's id, as
GET /events/{slug}/messageslists it.
Response fields
bodystring- The body as prose: what was typed, with the composer's variables read as their fallbacks and its blocks and marks dropped.
body_sourcestring- The stored source as the composer keeps it, variables, blocks and marks included. Read-only.
previewstring- The line an inbox shows after the subject: plain words on one line, hidden in the email itself. Empty for none.
audiencestring- Who it goes to:
all,buyers,both,invited,waitlist,signups, ortype:<ticket type id>.waitlistis everyone on the event's waitlists except those who left, and each copy ends with that person's own link to leave.signupsis everyone who confirmed a sign-up on one of the event's widgets that collect emails, never anyone who withdrew; it takes no segment, and each copy ends with that person's own link to stop updates. segmentobject or null- The compose page's own filters, as it stores them (checked_in, confirmed, since, type, ostatus, …); null for none.
confirmedisyesorno: whether the attendee has said they are still coming. A since date begins at midnight in the event's timezone.pickedis a hand-picked set of ticket references, comma-separated, as the attendee list sends ticked rows. It narrows the ticket door to those tickets and the order door to whoever booked them, and unlike every other filter the set does not grow, so a scheduled message goes to the same people however the event changes. It must be a string of at most 2000 references, or the request is refused with 422invalid_body. Apickedvalue with no readable reference in it matches nobody. calendarboolean- Whether the message carries a calendar file for the event. Each recipient's copy is built from their own order, so somebody holding a timed ticket gets their session as well as the event.
filesarray of MessageFile- Files attached to the message, in order. Read only: attaching one is a browser upload.
files[].filenamestringfiles[].deliverystringemailwhen the file is sent with the message,linkwhen the email carries a download link to it instead,offlinewhen Tito took the file offline and it is sent neither way. Decided by the weight of everything attached, in order. An offline file adds no weight.- one of
emaillinkoffline statusstringdraft,scheduled,sending(on its hold, or going out),sent, orstopped(a sweep declined; seestop_reason).- one of
draftscheduledsendingsentstopped deliveredinteger- Sent emails the email provider reports as delivered to the recipient's mail server.
waitinginteger- Sent emails the provider hasn't reported on yet, usually for seconds. One it is still trying (a full mailbox, a slow server) waits up to a day before it counts as bounced.
bouncedinteger- Sent emails the recipient's mail server turned away: the address doesn't exist, or a full mailbox stayed full for a day.
not_sentinteger- Emails the provider didn't send, because an earlier email to the address bounced or was reported as spam.
marked_as_spaminteger- Sent emails the recipient reported as spam. Whatever of
sentthese five leave was sent through a mail transport that can't report back (SMTP, or a sender imported from Tito Classic or Pro). created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or nullemail_sender_address_idintegeremail_sender_address_idis the ID of the selected address, or0for a domain choice or a message that follows the event. When it is0, a positiveemail_sender_ididentifies the chosen domain;email_sender_id: 0means follow the event.email_sender_idinteger- The sender chosen for this message (
GET /email-senders), 0 when it sends from its event's sender. This is the sender asked for: one that is not live when the message goes is replaced by the event's.
Responses
200The message.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/messages/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/messages/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"subject": "Doors open at 8",
"body": "See you there.\n\nBring ID.",
"body_source": "See you there.\n\nBring ID.",
"preview": "Bring your ticket and a jacket",
"audience": "all",
"segment": {
"checked_in": "no"
},
"look": "",
"calendar": false,
"files": [
{
"filename": "floor-plan.pdf",
"size": 284193,
"delivery": "email"
}
],
"status": "draft",
"scheduled_for": null,
"send_after": null,
"scheduled_count": 0,
"sent": 0,
"failed": 0,
"pending": 0,
"logged": 0,
"delivered": 401,
"waiting": 0,
"bounced": 6,
"not_sent": 3,
"marked_as_spam": 2,
"stop_reason": "",
"scheduled_by": "",
"stopped_by": "",
"created_at": "2026-09-07T20:50:00Z",
"updated_at": "2026-09-07T20:50:00Z",
"email_sender_address_id": 1,
"email_sender_id": 0,
"kind": "bulk"
}
Edit a message
PATCH /events/{slug}/messages/{id}
Absent fields are unchanged; an explicit null and an unknown field are refused; segment: null clears the narrowing. A message on the clock can be reworded but not emptied (409 scheduled); a sent one cannot be edited (409 sent). The composer's per-language variants are replaced by none, as its own save replaces them wholesale. The API writes PROSE: what you send as body is stored as literal text through the composer's own writer, so figh syntax and mark syntax typed into it arrive as text and never as a directive or a mark. The composer's variables, blocks, marks and per-language variants are its own affordances; a message written there answers its prose reading in body and its full source in body_source.
Requires messages.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The message's id, as
GET /events/{slug}/messageslists it.
Request body
previewstring- The line an inbox shows after the subject. Plain words; line breaks become spaces. Empty for none.
segmentobject or null- The compose page's own filters, as it stores them (checked_in, confirmed, since, type, ostatus, …); null for none.
confirmedisyesorno: whether the attendee has said they are still coming. A since date begins at midnight in the event's timezone.pickedis a hand-picked set of ticket references, comma-separated, as the attendee list sends ticked rows. It narrows the ticket door to those tickets and the order door to whoever booked them, and unlike every other filter the set does not grow, so a scheduled message goes to the same people however the event changes. It must be a string of at most 2000 references, or the request is refused with 422invalid_body. Apickedvalue with no readable reference in it matches nobody. A key the compose page has no filter for, or a value of the wrong type (typeis a ticket type's id as a number, not a string), is refused with 422invalid_bodynaming it, rather than stored as no narrowing and sent to the whole audience. email_sender_address_idintegeremail_sender_address_idis optional. A positive ID selects that exact address from theaddressesreturned byGET /email-senders. Its sender must be live; a saved message can keep its unchanged choice while the sender is pending. If both IDs are positive, the address must belong toemail_sender_id, or the request returns HTTP 422invalid_body. Address ID0follows the event unless a positiveemail_sender_idselects a domain. Omitting both fields from a messagePATCHpreserves its choice. A domain-onlyPATCHrepeating the current domain also preserves a selected address. To select the domain explicitly, send its ID with address ID0.email_sender_idinteger- The sender this message goes from: one of the account's live senders (
GET /email-senders), or 0 to send from the event's sender. Left out on a create, it is 0; left out on a PATCH, it is unchanged. Any other id is refused with422 invalid_body, except the message's own current choice.POST …/{id}/sendsends from what is stored.
Response fields
bodystring- The body as prose: what was typed, with the composer's variables read as their fallbacks and its blocks and marks dropped.
body_sourcestring- The stored source as the composer keeps it, variables, blocks and marks included. Read-only.
previewstring- The line an inbox shows after the subject: plain words on one line, hidden in the email itself. Empty for none.
audiencestring- Who it goes to:
all,buyers,both,invited,waitlist,signups, ortype:<ticket type id>.waitlistis everyone on the event's waitlists except those who left, and each copy ends with that person's own link to leave.signupsis everyone who confirmed a sign-up on one of the event's widgets that collect emails, never anyone who withdrew; it takes no segment, and each copy ends with that person's own link to stop updates. segmentobject or null- The compose page's own filters, as it stores them (checked_in, confirmed, since, type, ostatus, …); null for none.
confirmedisyesorno: whether the attendee has said they are still coming. A since date begins at midnight in the event's timezone.pickedis a hand-picked set of ticket references, comma-separated, as the attendee list sends ticked rows. It narrows the ticket door to those tickets and the order door to whoever booked them, and unlike every other filter the set does not grow, so a scheduled message goes to the same people however the event changes. It must be a string of at most 2000 references, or the request is refused with 422invalid_body. Apickedvalue with no readable reference in it matches nobody. calendarboolean- Whether the message carries a calendar file for the event. Each recipient's copy is built from their own order, so somebody holding a timed ticket gets their session as well as the event.
filesarray of MessageFile- Files attached to the message, in order. Read only: attaching one is a browser upload.
files[].filenamestringfiles[].deliverystringemailwhen the file is sent with the message,linkwhen the email carries a download link to it instead,offlinewhen Tito took the file offline and it is sent neither way. Decided by the weight of everything attached, in order. An offline file adds no weight.- one of
emaillinkoffline statusstringdraft,scheduled,sending(on its hold, or going out),sent, orstopped(a sweep declined; seestop_reason).- one of
draftscheduledsendingsentstopped deliveredinteger- Sent emails the email provider reports as delivered to the recipient's mail server.
waitinginteger- Sent emails the provider hasn't reported on yet, usually for seconds. One it is still trying (a full mailbox, a slow server) waits up to a day before it counts as bounced.
bouncedinteger- Sent emails the recipient's mail server turned away: the address doesn't exist, or a full mailbox stayed full for a day.
not_sentinteger- Emails the provider didn't send, because an earlier email to the address bounced or was reported as spam.
marked_as_spaminteger- Sent emails the recipient reported as spam. Whatever of
sentthese five leave was sent through a mail transport that can't report back (SMTP, or a sender imported from Tito Classic or Pro). created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or nullemail_sender_address_idintegeremail_sender_address_idis the ID of the selected address, or0for a domain choice or a message that follows the event. When it is0, a positiveemail_sender_ididentifies the chosen domain;email_sender_id: 0means follow the event.email_sender_idinteger- The sender chosen for this message (
GET /email-senders), 0 when it sends from its event's sender. This is the sender asked for: one that is not live when the message goes is replaced by the event's.
Responses
200The message as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The message has gone (sent), or is on the clock and the edit would empty its subject or body (scheduled).422As for create (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/messages/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"subject":"Doors open at 8"}'
// 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/messages/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"subject": "Doors open at 8"
}),
});
const data = await res.json();
{
"id": 3,
"subject": "Doors open at 8",
"body": "See you there.\n\nBring ID.",
"body_source": "See you there.\n\nBring ID.",
"preview": "Bring your ticket and a jacket",
"audience": "all",
"segment": {
"checked_in": "no"
},
"look": "",
"calendar": false,
"files": [
{
"filename": "floor-plan.pdf",
"size": 284193,
"delivery": "email"
}
],
"status": "draft",
"scheduled_for": null,
"send_after": null,
"scheduled_count": 0,
"sent": 0,
"failed": 0,
"pending": 0,
"logged": 0,
"delivered": 401,
"waiting": 0,
"bounced": 6,
"not_sent": 3,
"marked_as_spam": 2,
"stop_reason": "",
"scheduled_by": "",
"stopped_by": "",
"created_at": "2026-09-07T20:50:00Z",
"updated_at": "2026-09-07T20:50:00Z",
"email_sender_address_id": 1,
"email_sender_id": 0,
"kind": "bulk"
}
Send a message, now or later
POST /events/{slug}/messages/{id}/send
Arms the send. With no body (or no at) the message goes after the same sixty-second hold the compose page's Send button gets, during which stop still catches it; with at (RFC 3339, in the future, at most six months out) it goes then. The audience is resolved now for scheduled_count and again at the moment it goes. Refuses a message with no subject, no body, or nobody to send to. A message already on the clock is re-armed with the new moment.
Requires messages.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The message's id, as
GET /events/{slug}/messageslists it.
Request body
atstring- When to send, RFC 3339, in the future and at most six months out. Absent means now, after the sixty-second hold.
Response fields
bodystring- The body as prose: what was typed, with the composer's variables read as their fallbacks and its blocks and marks dropped.
body_sourcestring- The stored source as the composer keeps it, variables, blocks and marks included. Read-only.
previewstring- The line an inbox shows after the subject: plain words on one line, hidden in the email itself. Empty for none.
audiencestring- Who it goes to:
all,buyers,both,invited,waitlist,signups, ortype:<ticket type id>.waitlistis everyone on the event's waitlists except those who left, and each copy ends with that person's own link to leave.signupsis everyone who confirmed a sign-up on one of the event's widgets that collect emails, never anyone who withdrew; it takes no segment, and each copy ends with that person's own link to stop updates. segmentobject or null- The compose page's own filters, as it stores them (checked_in, confirmed, since, type, ostatus, …); null for none.
confirmedisyesorno: whether the attendee has said they are still coming. A since date begins at midnight in the event's timezone.pickedis a hand-picked set of ticket references, comma-separated, as the attendee list sends ticked rows. It narrows the ticket door to those tickets and the order door to whoever booked them, and unlike every other filter the set does not grow, so a scheduled message goes to the same people however the event changes. It must be a string of at most 2000 references, or the request is refused with 422invalid_body. Apickedvalue with no readable reference in it matches nobody. calendarboolean- Whether the message carries a calendar file for the event. Each recipient's copy is built from their own order, so somebody holding a timed ticket gets their session as well as the event.
filesarray of MessageFile- Files attached to the message, in order. Read only: attaching one is a browser upload.
files[].filenamestringfiles[].deliverystringemailwhen the file is sent with the message,linkwhen the email carries a download link to it instead,offlinewhen Tito took the file offline and it is sent neither way. Decided by the weight of everything attached, in order. An offline file adds no weight.- one of
emaillinkoffline statusstringdraft,scheduled,sending(on its hold, or going out),sent, orstopped(a sweep declined; seestop_reason).- one of
draftscheduledsendingsentstopped deliveredinteger- Sent emails the email provider reports as delivered to the recipient's mail server.
waitinginteger- Sent emails the provider hasn't reported on yet, usually for seconds. One it is still trying (a full mailbox, a slow server) waits up to a day before it counts as bounced.
bouncedinteger- Sent emails the recipient's mail server turned away: the address doesn't exist, or a full mailbox stayed full for a day.
not_sentinteger- Emails the provider didn't send, because an earlier email to the address bounced or was reported as spam.
marked_as_spaminteger- Sent emails the recipient reported as spam. Whatever of
sentthese five leave was sent through a mail transport that can't report back (SMTP, or a sender imported from Tito Classic or Pro). created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or nullemail_sender_address_idintegeremail_sender_address_idis the ID of the selected address, or0for a domain choice or a message that follows the event. When it is0, a positiveemail_sender_ididentifies the chosen domain;email_sender_id: 0means follow the event.email_sender_idinteger- The sender chosen for this message (
GET /email-senders), 0 when it sends from its event's sender. This is the sender asked for: one that is not live when the message goes is replaced by the event's.
Responses
200The message on the clock.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The message has already gone (sent).422No subject or no body (invalid_body); nobody matches the audience and segment (no_recipients);atmalformed, in the past or too far out (invalid_dates).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/messages/1/send \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"at":"2026-10-01T09:00:00Z"}'
// 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/messages/1/send", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"at": "2026-10-01T09:00:00Z"
}),
});
const data = await res.json();
{
"id": 3,
"subject": "Doors open at 8",
"body": "See you there.\n\nBring ID.",
"body_source": "See you there.\n\nBring ID.",
"preview": "Bring your ticket and a jacket",
"audience": "all",
"segment": {
"checked_in": "no"
},
"look": "",
"calendar": false,
"files": [
{
"filename": "floor-plan.pdf",
"size": 284193,
"delivery": "email"
}
],
"status": "draft",
"scheduled_for": null,
"send_after": null,
"scheduled_count": 0,
"sent": 0,
"failed": 0,
"pending": 0,
"logged": 0,
"delivered": 401,
"waiting": 0,
"bounced": 6,
"not_sent": 3,
"marked_as_spam": 2,
"stop_reason": "",
"scheduled_by": "",
"stopped_by": "",
"created_at": "2026-09-07T20:50:00Z",
"updated_at": "2026-09-07T20:50:00Z",
"email_sender_address_id": 1,
"email_sender_id": 0,
"kind": "bulk"
}
Skip the rest of the send hold
POST /events/{slug}/messages/{id}/send-now
Burns the remainder of the sixty-second hold a send with no at arms: the fuse moves to now and the sweep is asked to look. Nothing else changes — the audience is still resolved at send time. 409 not_holding for anything that is not inside its hold, including a message scheduled for a named moment (move that with send or PATCH).
Requires messages.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The message's id, as
GET /events/{slug}/messageslists it.
Response fields
bodystring- The body as prose: what was typed, with the composer's variables read as their fallbacks and its blocks and marks dropped.
body_sourcestring- The stored source as the composer keeps it, variables, blocks and marks included. Read-only.
previewstring- The line an inbox shows after the subject: plain words on one line, hidden in the email itself. Empty for none.
audiencestring- Who it goes to:
all,buyers,both,invited,waitlist,signups, ortype:<ticket type id>.waitlistis everyone on the event's waitlists except those who left, and each copy ends with that person's own link to leave.signupsis everyone who confirmed a sign-up on one of the event's widgets that collect emails, never anyone who withdrew; it takes no segment, and each copy ends with that person's own link to stop updates. segmentobject or null- The compose page's own filters, as it stores them (checked_in, confirmed, since, type, ostatus, …); null for none.
confirmedisyesorno: whether the attendee has said they are still coming. A since date begins at midnight in the event's timezone.pickedis a hand-picked set of ticket references, comma-separated, as the attendee list sends ticked rows. It narrows the ticket door to those tickets and the order door to whoever booked them, and unlike every other filter the set does not grow, so a scheduled message goes to the same people however the event changes. It must be a string of at most 2000 references, or the request is refused with 422invalid_body. Apickedvalue with no readable reference in it matches nobody. calendarboolean- Whether the message carries a calendar file for the event. Each recipient's copy is built from their own order, so somebody holding a timed ticket gets their session as well as the event.
filesarray of MessageFile- Files attached to the message, in order. Read only: attaching one is a browser upload.
files[].filenamestringfiles[].deliverystringemailwhen the file is sent with the message,linkwhen the email carries a download link to it instead,offlinewhen Tito took the file offline and it is sent neither way. Decided by the weight of everything attached, in order. An offline file adds no weight.- one of
emaillinkoffline statusstringdraft,scheduled,sending(on its hold, or going out),sent, orstopped(a sweep declined; seestop_reason).- one of
draftscheduledsendingsentstopped deliveredinteger- Sent emails the email provider reports as delivered to the recipient's mail server.
waitinginteger- Sent emails the provider hasn't reported on yet, usually for seconds. One it is still trying (a full mailbox, a slow server) waits up to a day before it counts as bounced.
bouncedinteger- Sent emails the recipient's mail server turned away: the address doesn't exist, or a full mailbox stayed full for a day.
not_sentinteger- Emails the provider didn't send, because an earlier email to the address bounced or was reported as spam.
marked_as_spaminteger- Sent emails the recipient reported as spam. Whatever of
sentthese five leave was sent through a mail transport that can't report back (SMTP, or a sender imported from Tito Classic or Pro). created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or nullemail_sender_address_idintegeremail_sender_address_idis the ID of the selected address, or0for a domain choice or a message that follows the event. When it is0, a positiveemail_sender_ididentifies the chosen domain;email_sender_id: 0means follow the event.email_sender_idinteger- The sender chosen for this message (
GET /email-senders), 0 when it sends from its event's sender. This is the sender asked for: one that is not live when the message goes is replaced by the event's.
Responses
200The message, with its fuse now.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Not inside a send hold (not_holding).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/messages/1/send-now \
-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/messages/1/send-now", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"subject": "Doors open at 8",
"body": "See you there.\n\nBring ID.",
"body_source": "See you there.\n\nBring ID.",
"preview": "Bring your ticket and a jacket",
"audience": "all",
"segment": {
"checked_in": "no"
},
"look": "",
"calendar": false,
"files": [
{
"filename": "floor-plan.pdf",
"size": 284193,
"delivery": "email"
}
],
"status": "draft",
"scheduled_for": null,
"send_after": null,
"scheduled_count": 0,
"sent": 0,
"failed": 0,
"pending": 0,
"logged": 0,
"delivered": 401,
"waiting": 0,
"bounced": 6,
"not_sent": 3,
"marked_as_spam": 2,
"stop_reason": "",
"scheduled_by": "",
"stopped_by": "",
"created_at": "2026-09-07T20:50:00Z",
"updated_at": "2026-09-07T20:50:00Z",
"email_sender_address_id": 1,
"email_sender_id": 0,
"kind": "bulk"
}
Take a message off the clock
POST /events/{slug}/messages/{id}/stop
Cancels a hold or a schedule; the message goes back to a draft with its words intact. 409 not_scheduled when it was not on the clock — a sent message cannot be recalled.
Requires messages.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The message's id, as
GET /events/{slug}/messageslists it.
Response fields
bodystring- The body as prose: what was typed, with the composer's variables read as their fallbacks and its blocks and marks dropped.
body_sourcestring- The stored source as the composer keeps it, variables, blocks and marks included. Read-only.
previewstring- The line an inbox shows after the subject: plain words on one line, hidden in the email itself. Empty for none.
audiencestring- Who it goes to:
all,buyers,both,invited,waitlist,signups, ortype:<ticket type id>.waitlistis everyone on the event's waitlists except those who left, and each copy ends with that person's own link to leave.signupsis everyone who confirmed a sign-up on one of the event's widgets that collect emails, never anyone who withdrew; it takes no segment, and each copy ends with that person's own link to stop updates. segmentobject or null- The compose page's own filters, as it stores them (checked_in, confirmed, since, type, ostatus, …); null for none.
confirmedisyesorno: whether the attendee has said they are still coming. A since date begins at midnight in the event's timezone.pickedis a hand-picked set of ticket references, comma-separated, as the attendee list sends ticked rows. It narrows the ticket door to those tickets and the order door to whoever booked them, and unlike every other filter the set does not grow, so a scheduled message goes to the same people however the event changes. It must be a string of at most 2000 references, or the request is refused with 422invalid_body. Apickedvalue with no readable reference in it matches nobody. calendarboolean- Whether the message carries a calendar file for the event. Each recipient's copy is built from their own order, so somebody holding a timed ticket gets their session as well as the event.
filesarray of MessageFile- Files attached to the message, in order. Read only: attaching one is a browser upload.
files[].filenamestringfiles[].deliverystringemailwhen the file is sent with the message,linkwhen the email carries a download link to it instead,offlinewhen Tito took the file offline and it is sent neither way. Decided by the weight of everything attached, in order. An offline file adds no weight.- one of
emaillinkoffline statusstringdraft,scheduled,sending(on its hold, or going out),sent, orstopped(a sweep declined; seestop_reason).- one of
draftscheduledsendingsentstopped deliveredinteger- Sent emails the email provider reports as delivered to the recipient's mail server.
waitinginteger- Sent emails the provider hasn't reported on yet, usually for seconds. One it is still trying (a full mailbox, a slow server) waits up to a day before it counts as bounced.
bouncedinteger- Sent emails the recipient's mail server turned away: the address doesn't exist, or a full mailbox stayed full for a day.
not_sentinteger- Emails the provider didn't send, because an earlier email to the address bounced or was reported as spam.
marked_as_spaminteger- Sent emails the recipient reported as spam. Whatever of
sentthese five leave was sent through a mail transport that can't report back (SMTP, or a sender imported from Tito Classic or Pro). created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or nullemail_sender_address_idintegeremail_sender_address_idis the ID of the selected address, or0for a domain choice or a message that follows the event. When it is0, a positiveemail_sender_ididentifies the chosen domain;email_sender_id: 0means follow the event.email_sender_idinteger- The sender chosen for this message (
GET /email-senders), 0 when it sends from its event's sender. This is the sender asked for: one that is not live when the message goes is replaced by the event's.
Responses
200The message, a draft again.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Not on the clock (not_scheduled).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/messages/1/stop \
-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/messages/1/stop", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 3,
"subject": "Doors open at 8",
"body": "See you there.\n\nBring ID.",
"body_source": "See you there.\n\nBring ID.",
"preview": "Bring your ticket and a jacket",
"audience": "all",
"segment": {
"checked_in": "no"
},
"look": "",
"calendar": false,
"files": [
{
"filename": "floor-plan.pdf",
"size": 284193,
"delivery": "email"
}
],
"status": "draft",
"scheduled_for": null,
"send_after": null,
"scheduled_count": 0,
"sent": 0,
"failed": 0,
"pending": 0,
"logged": 0,
"delivered": 401,
"waiting": 0,
"bounced": 6,
"not_sent": 3,
"marked_as_spam": 2,
"stop_reason": "",
"scheduled_by": "",
"stopped_by": "",
"created_at": "2026-09-07T20:50:00Z",
"updated_at": "2026-09-07T20:50:00Z",
"email_sender_address_id": 1,
"email_sender_id": 0,
"kind": "bulk"
}
Recipients
GET /events/{slug}/outbox
API message records include a kind value of bulk or individual, and message lists can filter by kind. The event outbox endpoint backs a sent message's Recipients page in the admin: it accepts message_id, status, q, and page, and returns saved email records scoped to that event.
Requires messages.view
Path parameters
slugstring required- The event's slug.
Query parameters
message_idintegerstatusstringdelivered,didnt_arrive(bounced, not sent or marked as spam),waiting, or an email status:sent,pending,failed,loggedorcanceled.sentis every email handed over, whatever the provider said about it afterwards.qstringpageinteger
Response fields
emailsarray of OutboxEmailemails[].idintegeremails[].message_idintegeremails[].recipientstringemails[].subjectstringemails[].statusstringemails[].deliverystring- What the email provider said became of a sent email. Empty when there is nothing to report: not sent yet, or sent through a mail transport that can't report back (SMTP, or a sender imported from Tito Classic or Pro), and
statussays what it is. - one of
deliveredwaitingbouncednot_sentmarked_as_spam emails[].delivery_reasonstring- The provider's classification of a bounce, a delay or a spam report, such as
NoEmailorMailboxFull. Empty when it gave none. emails[].fixed_tostring- The address this email's recipient was corrected to on their ticket or order, when it hasn't been sent there yet.
recipientstays the address it was sent to. Empty otherwise. emails[].created_atintegertotalintegerpageintegerpagesinteger
Responses
200Messages.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/outbox \
-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/outbox", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"emails": [
{
"id": 1,
"message_id": 1,
"recipient": "example",
"subject": "example",
"status": "ticket",
"delivery": "",
"delivery_reason": "example",
"fixed_to": "example",
"created_at": 1
}
],
"total": 42,
"page": 1,
"pages": 1
}
One recipient's email
GET /events/{slug}/outbox/{id}
One saved email from the event's outbox, with its body and HTML, as a sent message's Recipients page shows it.
Requires messages.view
Path parameters
slugstring required- The event's slug.
idinteger required
Response fields
idintegermessage_idintegerrecipientstringsubjectstringstatusstringdeliverystring- What the email provider said became of a sent email. Empty when there is nothing to report: not sent yet, or sent through a mail transport that can't report back (SMTP, or a sender imported from Tito Classic or Pro), and
statussays what it is. - one of
deliveredwaitingbouncednot_sentmarked_as_spam delivery_reasonstring- The provider's classification of a bounce, a delay or a spam report, such as
NoEmailorMailboxFull. Empty when it gave none. fixed_tostring- The address this email's recipient was corrected to on their ticket or order, when it hasn't been sent there yet.
recipientstays the address it was sent to. Empty otherwise. created_atintegerbodystringhtmlstringsenderstring
Responses
200Messages.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/outbox/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/outbox/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 1,
"message_id": 1,
"recipient": "example",
"subject": "example",
"status": "ticket",
"delivery": "",
"delivery_reason": "example",
"fixed_to": "example",
"created_at": 1,
"body": "example",
"html": "example",
"sender": "example"
}
On-site
List the card readers
GET /events/{slug}/terminal-readers
The event's card readers: on-site payments, one row per physical reader. REGISTERING a reader is not served here, and that is an honest limit rather than an omission — it claims a pairing code read off the reader's own screen, and that code is single-use, so a caller getting it wrong would burn it with nobody standing at the device to fetch another. It stays the admin's form. Live readers first, then archived ones. The door URL and PIN are NOT in the list: they are the credentials to a payment screen, and a list is read far more often than a reader is set up.
Requires events.view
Path parameters
slugstring required- The event's slug.
Response fields
readersarray of TerminalReaderreaders[].collect_nameboolean- Offer an optional name field on the reader before payment. Omit on PATCH to leave unchanged.
readers[].collect_emailboolean- Offer an optional email field on the reader before payment. A supplied email receives the Tito order confirmation after payment. Omit on PATCH to leave unchanged.
readers[].ticket_type_idsarray of integer or null- Ticket types offered on this Door page. Null offers all saleable types; an empty array offers none. Omit on PATCH to leave unchanged.
readers[].idintegerreaders[].door_urlstring- The door the person at the reader opens. On the reader's own record only, and absent once archived.
readers[].created_atstring (RFC 3339) or null
Responses
200The readers.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/terminal-readers \
-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/terminal-readers", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"readers": [
{
"collect_name": true,
"collect_email": true,
"ticket_type_ids": [
1
],
"id": 3,
"ref": "R4CAZFD",
"label": "Door 2",
"stripe_reader_id": "tmr_FQ2b0Lm",
"location_id": "tml_main",
"sales": 41,
"archived": false,
"door_url": "https://acct.go.tito.io/door/_9AUNJCBG6eclaz8nCzYqg",
"door_pin": "123456",
"created_at": "2026-09-08T11:40:00Z"
}
]
}
Get a card reader
GET /events/{slug}/terminal-readers/{id}
One reader, WITH its door URL and PIN — the credentials the person at the reader signs in with. An archived reader's door does not answer, so its credentials are not quoted. Credential-bearing responses set Cache-Control: no-store. Terminal-reader request and response bodies are omitted from API request logs.
Requires events.view
Path parameters
slugstring required- The event's slug.
idinteger required- The reader's id, as
GET /events/{slug}/terminal-readerslists it.
Response fields
collect_nameboolean- Offer an optional name field on the reader before payment. Omit on PATCH to leave unchanged.
collect_emailboolean- Offer an optional email field on the reader before payment. A supplied email receives the Tito order confirmation after payment. Omit on PATCH to leave unchanged.
ticket_type_idsarray of integer or null- Ticket types offered on this Door page. Null offers all saleable types; an empty array offers none. Omit on PATCH to leave unchanged.
idintegerdoor_urlstring- The door the person at the reader opens. On the reader's own record only, and absent once archived.
created_atstring (RFC 3339) or null
Responses
200The reader.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/terminal-readers/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/terminal-readers/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"collect_name": true,
"collect_email": true,
"ticket_type_ids": [
1
],
"id": 3,
"ref": "R4CAZFD",
"label": "Door 2",
"stripe_reader_id": "tmr_FQ2b0Lm",
"location_id": "tml_main",
"sales": 41,
"archived": false,
"door_url": "https://acct.go.tito.io/door/_9AUNJCBG6eclaz8nCzYqg",
"door_pin": "123456",
"created_at": "2026-09-08T11:40:00Z"
}
Rename a card reader
PATCH /events/{slug}/terminal-readers/{id}
The label is the only thing about a reader anybody names: everything else is the device's, or Stripe's. Credential-bearing responses set Cache-Control: no-store. Terminal-reader request and response bodies are omitted from API request logs.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The reader's id, as
GET /events/{slug}/terminal-readerslists it.
Request body
collect_nameboolean- Offer an optional name field on the reader before payment. Omit on PATCH to leave unchanged.
collect_emailboolean- Offer an optional email field on the reader before payment. A supplied email receives the Tito order confirmation after payment. Omit on PATCH to leave unchanged.
ticket_type_idsarray of integer or null- Ticket types offered on this Door page. Null offers all saleable types; an empty array offers none. Omit on PATCH to leave unchanged.
Response fields
collect_nameboolean- Offer an optional name field on the reader before payment. Omit on PATCH to leave unchanged.
collect_emailboolean- Offer an optional email field on the reader before payment. A supplied email receives the Tito order confirmation after payment. Omit on PATCH to leave unchanged.
ticket_type_idsarray of integer or null- Ticket types offered on this Door page. Null offers all saleable types; an empty array offers none. Omit on PATCH to leave unchanged.
idintegerdoor_urlstring- The door the person at the reader opens. On the reader's own record only, and absent once archived.
created_atstring (RFC 3339) or null
Responses
200The reader as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The reader has been archived, which is the end of one — nothing is left to do to it (archived).422The body was unreadable or carried an unknown field, or the label was blank (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/terminal-readers/1 \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"collect_name":true}'
// 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/terminal-readers/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"collect_name": true
}),
});
const data = await res.json();
{
"collect_name": true,
"collect_email": true,
"ticket_type_ids": [
1
],
"id": 3,
"ref": "R4CAZFD",
"label": "Door 2",
"stripe_reader_id": "tmr_FQ2b0Lm",
"location_id": "tml_main",
"sales": 41,
"archived": false,
"door_url": "https://acct.go.tito.io/door/_9AUNJCBG6eclaz8nCzYqg",
"door_pin": "123456",
"created_at": "2026-09-08T11:40:00Z"
}
Archive a card reader
POST /events/{slug}/terminal-readers/{id}/archive
ENDS a reader: its door stops answering and there is nothing left to do to it. The sales it rang up stay where they are — archiving a reader must never unpick the money it took, which is why this is not a delete.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The reader's id, as
GET /events/{slug}/terminal-readerslists it.
Response fields
collect_nameboolean- Offer an optional name field on the reader before payment. Omit on PATCH to leave unchanged.
collect_emailboolean- Offer an optional email field on the reader before payment. A supplied email receives the Tito order confirmation after payment. Omit on PATCH to leave unchanged.
ticket_type_idsarray of integer or null- Ticket types offered on this Door page. Null offers all saleable types; an empty array offers none. Omit on PATCH to leave unchanged.
idintegerdoor_urlstring- The door the person at the reader opens. On the reader's own record only, and absent once archived.
created_atstring (RFC 3339) or null
Responses
200The archived reader.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The reader has been archived, which is the end of one — nothing is left to do to it (archived).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/terminal-readers/1/archive \
-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/terminal-readers/1/archive", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"collect_name": true,
"collect_email": true,
"ticket_type_ids": [
1
],
"id": 3,
"ref": "R4CAZFD",
"label": "Door 2",
"stripe_reader_id": "tmr_FQ2b0Lm",
"location_id": "tml_main",
"sales": 41,
"archived": false,
"door_url": "https://acct.go.tito.io/door/_9AUNJCBG6eclaz8nCzYqg",
"door_pin": "123456",
"created_at": "2026-09-08T11:40:00Z"
}
Rotate a reader's door credentials
POST /events/{slug}/terminal-readers/{id}/rotate
Mints a fresh door URL and PIN, killing the old pair — the escape hatch for a link or a number that got out. The answer carries the new ones, which is the whole point of asking. Credential-bearing responses set Cache-Control: no-store. Terminal-reader request and response bodies are omitted from API request logs.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The reader's id, as
GET /events/{slug}/terminal-readerslists it.
Response fields
collect_nameboolean- Offer an optional name field on the reader before payment. Omit on PATCH to leave unchanged.
collect_emailboolean- Offer an optional email field on the reader before payment. A supplied email receives the Tito order confirmation after payment. Omit on PATCH to leave unchanged.
ticket_type_idsarray of integer or null- Ticket types offered on this Door page. Null offers all saleable types; an empty array offers none. Omit on PATCH to leave unchanged.
idintegerdoor_urlstring- The door the person at the reader opens. On the reader's own record only, and absent once archived.
created_atstring (RFC 3339) or null
Responses
200The reader, with its new credentials.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The reader has been archived, which is the end of one — nothing is left to do to it (archived).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/terminal-readers/1/rotate \
-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/terminal-readers/1/rotate", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"collect_name": true,
"collect_email": true,
"ticket_type_ids": [
1
],
"id": 3,
"ref": "R4CAZFD",
"label": "Door 2",
"stripe_reader_id": "tmr_FQ2b0Lm",
"location_id": "tml_main",
"sales": 41,
"archived": false,
"door_url": "https://acct.go.tito.io/door/_9AUNJCBG6eclaz8nCzYqg",
"door_pin": "123456",
"created_at": "2026-09-08T11:40:00Z"
}
Badges
Get the badge design
GET /events/{slug}/badge-design
How this event's badges are drawn. An event has ONE design rather than a list of them, so this is a single resource with a read and an edit: there is no create and no delete. An event that has never designed a badge reads the PROPOSAL Tito would draw for it, with saved false to say nobody decided it. The two per-run numbers the panel offers — how many slots on the first sheet are already peeled off, and how many blanks to add for walk-ups — are deliberately absent: they belong to one print run rather than to the event, which is why the admin never stores them either.
Requires events.view
Path parameters
slugstring required- The event's slug.
Response fields
linesarray of BadgeLinelines[].keystring- "name" (the line every badge carries), "type" for the ticket type's own name, or "field:<id>" for an answer to a question this event asked.
lines[].sizestring- Millimetres of type ON the badge, absolute rather than relative to the stock — so changing stock does not re-flow it.
- one of
lms logoboolean- On a positioned design, whether the
logobox is on: the event's uploaded logo, from Customize, printed on each badge. On a stack design it is the flag as last sent, kept but not printed: a stack has no logo. paperstring- One of
papers' keys, or "custom" for a sheet size the organizer typed. It decides which presets are offerable and the page the PDF is converted at. - one of
a4lettercustom custom_width_mmnumber- One badge's width, present only while
sheetis "custom". Always clamped to the paper: a value wider than the page reads back the page's own width. custom_height_mmnumber- One badge's height, present only while
sheetis "custom". Always clamped to the paper. paper_width_mmnumber- The typed paper's width, present only while
paperis "custom". Always clamped to the paper bounds. paper_height_mmnumber- The typed paper's height, present only while
paperis "custom". Always clamped to the paper bounds. orderstring- The order the sheet comes off the printer: by name, or as people booked.
- one of
azbooked fontstring- The font everything on the badge is set in: one of
fonts, the fonts an organizer can choose for their event's pages.interunless the event chose another. - one of
interschibsted-groteskspace-grotesknewsreaderfraunceshost-groteskmanropesource-serif-4instrument-serifibm-plex-monobig-shoulders-displaynunitosource-sans-3 sheetsarray of BadgeSheet- The stock on offer for this
paper, so a caller can picksheetwithout knowing the list. Shorter on US Letter than on A4. sheets[].keystringsheets[].columnsintegersheets[].rowsintegerpapersarray of BadgePaper- The papers on offer, so a caller can pick
paperwithout knowing the list. papers[].keystringpositionedboolean- Whether the design is laid out as positioned boxes rather than as the stack in
lines. For a positioned design,linesis the stack an older reader would print from it. ignoredarray of string- Fields a PATCH accepted and did nothing with. Today only
scale_name, on a positioned design, where a box always fits its text. boxesarray of BadgeBox- The badge as positioned boxes, in millimetres on the design's current card. For a design that is not positioned yet, the layout the badge editor will open it with.
boxes[].keystring- What the box prints:
name,first_nameandlast_name(the name in two parts, read from the buyer's own reading or the event's language),type(the ticket type),field:<id>(an answer),organizer(the account's name, as Settings sets it),logo(the event's logo, as Customize sets it),event(the event name) orref(the ticket reference). Whenfirst_nameandlast_nameare on and one stands above the other, each is printed against the line between them, so the two read as one name with the same small gap on every badge, and whichever is higher prints first. boxes[].same_sizeboolean- Whether the box is set at one size on every badge: the largest the event's longest value fits.
boxes[].size_ptinteger- Present when the box is set at a size of its own, in points: the text prints at it, and smaller only where a long value needs it.
boxes[].match_name_partsboolean- Only on
first_nameandlast_name: whether the two print at one size, the smaller of their two fits. boxes[].stylestring- Absent for
eventandref, which keep the badge foot's own type. - one of
strongplainmuted
Responses
200The design.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/badge-design \
-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/badge-design", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"saved": false,
"lines": [
{
"key": "name",
"size": "l"
},
{
"key": "type",
"size": "s"
}
],
"logo": false,
"sheet": "8",
"paper": "a4",
"custom_width_mm": 85.6,
"custom_height_mm": 54,
"paper_width_mm": 240,
"paper_height_mm": 120,
"crop": false,
"scale_name": false,
"show_event": true,
"show_ref": true,
"order": "az",
"font": "inter",
"fonts": [
"inter",
"fraunces"
],
"sheets": [
{
"columns": 2,
"height_mm": 68,
"key": "8",
"per_sheet": 8,
"rows": 4,
"width_mm": 99
}
],
"papers": [
{
"height_mm": 297,
"key": "a4",
"width_mm": 210
}
],
"positioned": false,
"ignored": [
"scale_name"
],
"boxes": [
{
"key": "name",
"x_mm": 4,
"y_mm": 20.4,
"width_mm": 91.1,
"height_mm": 10.2,
"wrap": false,
"same_size": false,
"size_pt": 36,
"match_name_parts": true,
"align": "left",
"style": "strong",
"on": true
}
]
}
Update the badge design
PATCH /events/{slug}/badge-design
Absent means unchanged; lines replaces the stack whole, because they are an ORDER as much as a set. The NAME line is never optional: a body that leaves it out gets it back at the top rather than a refusal, the panel renders it locked for the same reason, and a badge without a name is not a badge. A line repeated is dropped — it would be drawn twice on top of itself.
Requires events.manage
Path parameters
slugstring required- The event's slug.
Request body
boxesarray of BadgeBoxInput- Replaces the positioned layout whole, in millimetres on the design's card, and positions a design that was a stack. The name box must be on (422
no_name_box), whole or split but never both: withfirst_nameorlast_nameon,nameis switched off; a key other thanname,first_name,last_name,type,organizer,logo,event,reforfield:<id>is refused (422unknown_box_key). Boxes are kept inside the card at no less than their minimum size. boxes[].keystring requiredboxes[].x_mmnumber requiredboxes[].y_mmnumber requiredboxes[].width_mmnumber requiredboxes[].height_mmnumber requiredboxes[].wrapbooleanboxes[].same_sizebooleanboxes[].size_ptinteger- A size of the box's own, in points, from 9 to 200: the text prints at it, and smaller only where a long value needs it. 0 or absent sets the text as large as it fits. Overrides
same_size. boxes[].match_name_partsboolean- Only on
first_nameandlast_name. Defaults to true. False on either sets both to fit their own boxes. linesarray of BadgeLine- Replaces the stack whole. The name line is added back at the top if it is missing.
lines[].keystring required- "name" (the line every badge carries), "type" for the ticket type's own name, or "field:<id>" for an answer to a question this event asked.
lines[].sizestring required- Millimetres of type ON the badge, absolute rather than relative to the stock — so changing stock does not re-flow it.
- one of
lms logoboolean- On a positioned design, switches the
logobox on or off, keeping its place or placing it under what is there. On a stack design it is stored and prints nothing. sheetstring- One of the stock keys offered for
paper, or "custom" for a typed size. Applied afterpaper, so a preset the chosen paper cannot print is refused. paperstring- The sheet the run prints on. Applied before
sheet, because which presets a caller may name depends on it. "custom" takespaper_width_mmandpaper_height_mm. Changing the paper alone can move the stock: a preset the new paper cannot print falls back to the default stock for it, the same way the panel does, and the response says which sheet the design ended up with. Readsheetback rather than assuming it kept the one it had. - one of
a4lettercustom custom_width_mmnumber- One badge's width, meaning anything only while
sheetis "custom". Clamped to the paper rather than refused, the same way the panel clamps it. custom_height_mmnumber- One badge's height, meaning anything only while
sheetis "custom". Clamped to the paper. paper_width_mmnumber- The typed paper's width, meaning anything only while
paperis "custom". Clamped to the paper bounds. paper_height_mmnumber- The typed paper's height, meaning anything only while
paperis "custom". Clamped to the paper bounds. cropbooleanscale_namebooleanshow_eventbooleanshow_refbooleanfontstring- One of the design's
fonts. - one of
interschibsted-groteskspace-grotesknewsreaderfraunceshost-groteskmanropesource-serif-4instrument-serifibm-plex-monobig-shoulders-displaynunitosource-sans-3
Response fields
linesarray of BadgeLinelines[].keystring- "name" (the line every badge carries), "type" for the ticket type's own name, or "field:<id>" for an answer to a question this event asked.
lines[].sizestring- Millimetres of type ON the badge, absolute rather than relative to the stock — so changing stock does not re-flow it.
- one of
lms logoboolean- On a positioned design, whether the
logobox is on: the event's uploaded logo, from Customize, printed on each badge. On a stack design it is the flag as last sent, kept but not printed: a stack has no logo. paperstring- One of
papers' keys, or "custom" for a sheet size the organizer typed. It decides which presets are offerable and the page the PDF is converted at. - one of
a4lettercustom custom_width_mmnumber- One badge's width, present only while
sheetis "custom". Always clamped to the paper: a value wider than the page reads back the page's own width. custom_height_mmnumber- One badge's height, present only while
sheetis "custom". Always clamped to the paper. paper_width_mmnumber- The typed paper's width, present only while
paperis "custom". Always clamped to the paper bounds. paper_height_mmnumber- The typed paper's height, present only while
paperis "custom". Always clamped to the paper bounds. orderstring- The order the sheet comes off the printer: by name, or as people booked.
- one of
azbooked fontstring- The font everything on the badge is set in: one of
fonts, the fonts an organizer can choose for their event's pages.interunless the event chose another. - one of
interschibsted-groteskspace-grotesknewsreaderfraunceshost-groteskmanropesource-serif-4instrument-serifibm-plex-monobig-shoulders-displaynunitosource-sans-3 sheetsarray of BadgeSheet- The stock on offer for this
paper, so a caller can picksheetwithout knowing the list. Shorter on US Letter than on A4. sheets[].keystringsheets[].columnsintegersheets[].rowsintegerpapersarray of BadgePaper- The papers on offer, so a caller can pick
paperwithout knowing the list. papers[].keystringpositionedboolean- Whether the design is laid out as positioned boxes rather than as the stack in
lines. For a positioned design,linesis the stack an older reader would print from it. ignoredarray of string- Fields a PATCH accepted and did nothing with. Today only
scale_name, on a positioned design, where a box always fits its text. boxesarray of BadgeBox- The badge as positioned boxes, in millimetres on the design's current card. For a design that is not positioned yet, the layout the badge editor will open it with.
boxes[].keystring- What the box prints:
name,first_nameandlast_name(the name in two parts, read from the buyer's own reading or the event's language),type(the ticket type),field:<id>(an answer),organizer(the account's name, as Settings sets it),logo(the event's logo, as Customize sets it),event(the event name) orref(the ticket reference). Whenfirst_nameandlast_nameare on and one stands above the other, each is printed against the line between them, so the two read as one name with the same small gap on every badge, and whichever is higher prints first. boxes[].same_sizeboolean- Whether the box is set at one size on every badge: the largest the event's longest value fits.
boxes[].size_ptinteger- Present when the box is set at a size of its own, in points: the text prints at it, and smaller only where a long value needs it.
boxes[].match_name_partsboolean- Only on
first_nameandlast_name: whether the two print at one size, the smaller of their two fits. boxes[].stylestring- Absent for
eventandref, which keep the badge foot's own type. - one of
strongplainmuted
Responses
200The design as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Somebody has changes in Badge settings that are not saved yet (unsaved_badge_changes). Badge settings works on a draft and its Save replaces the design whole, so a write here would be overwritten without anyone seeing it. Nothing is changed; save or discard the changes in Badge settings, then send the request again. Reading the design is never refused, and reads what is saved.422The body was unreadable or carried an unknown field, a line had no key, a size was not l/m/s, the paper is not one this prints on, the stock is not one this paper prints on, or the order is not az/booked (invalid_body). A custom width or height — badge or paper — outside its bounds is clamped rather than refused.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/badge-design \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"boxes":[{"key":"name","x_mm":4,"y_mm":20,"width_mm":91,"height_mm":10}]}'
// 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/badge-design", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"boxes": [
{
"key": "name",
"x_mm": 4,
"y_mm": 20,
"width_mm": 91,
"height_mm": 10
}
]
}),
});
const data = await res.json();
{
"saved": false,
"lines": [
{
"key": "name",
"size": "l"
},
{
"key": "type",
"size": "s"
}
],
"logo": false,
"sheet": "8",
"paper": "a4",
"custom_width_mm": 85.6,
"custom_height_mm": 54,
"paper_width_mm": 240,
"paper_height_mm": 120,
"crop": false,
"scale_name": false,
"show_event": true,
"show_ref": true,
"order": "az",
"font": "inter",
"fonts": [
"inter",
"fraunces"
],
"sheets": [
{
"columns": 2,
"height_mm": 68,
"key": "8",
"per_sheet": 8,
"rows": 4,
"width_mm": 99
}
],
"papers": [
{
"height_mm": 297,
"key": "a4",
"width_mm": 210
}
],
"positioned": false,
"ignored": [
"scale_name"
],
"boxes": [
{
"key": "name",
"x_mm": 4,
"y_mm": 20.4,
"width_mm": 91.1,
"height_mm": 10.2,
"wrap": false,
"same_size": false,
"size_pt": 36,
"match_name_parts": true,
"align": "left",
"style": "strong",
"on": true
}
]
}
Waitlist
List the waitlist
GET /events/{slug}/waitlist
Everyone on the event's waitlists, in the order they joined. A ticket type opens a waitlist when it is fully booked and its waitlist is on. A workshop option opens one when it is full and its offering's waitlist is on. Only ticket types' waitlists are listed unless you ask for workshop options' with kind or session_id. The counts cover every state for the kind and the ticket type or option you filter by, so a search or a state narrows the list but not the counts. A person's own link for leaving is never reported.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
Query parameters
ticket_type_idinteger- Only this ticket type's list.
kindstring- Which waitlists: ticket (ticket types' only), workshop (workshop options' only) or all. ticket unless
session_idis given, which means workshop. session_idinteger- Only this workshop option's list. An option on another event lists nobody.
statestring- Only people in this state.
qstring- Part of a name or email address. Case does not matter.
Response fields
waitlistarray of WaitlistEntrywaitlist[].idintegerwaitlist[].namestringwaitlist[].emailstringwaitlist[].ticket_type_idinteger or null- The ticket type whose waitlist they are on. null when they are waiting for a workshop option.
waitlist[].ticket_type_namestring or null- That ticket type's name. null when they are waiting for a workshop option.
waitlist[].session_idinteger or null- The workshop option whose waitlist they are on. null when they are waiting for a ticket type.
waitlist[].session_namestring or null- That option's name, or "" for an option with no name of its own. null when they are waiting for a ticket type.
waitlist[].offering_idinteger or null- The workshop offering the option belongs to. null when they are waiting for a ticket type.
waitlist[].offering_namestring or null- That offering's name. null when they are waiting for a ticket type.
waitlist[].ticket_refstring or null- The reference of the ticket that is waiting for the workshop option. null once the ticket goes to someone else or its workshop place is refunded, and when they are waiting for a ticket type.
waitlist[].statestring- waiting, offered (emailed about a free place, or offered one by hand, see held_until), took or left.
- one of
waitingofferedtookleft waitlist[].sourcestring- page when they joined from the event page, organizer when someone added them by hand, ticket when they joined a workshop option's waitlist from their ticket.
- one of
pageorganizerticket waitlist[].joined_atstring (RFC 3339)waitlist[].left_atstring (RFC 3339) or null- When they left or were removed. null while they are on the list.
waitlist[].positioninteger or null- Their place in the queue for this ticket type or workshop option, counting only people waiting. null unless they are waiting. For a workshop option, also null once the ticket goes to someone else or its workshop place is refunded.
waitlist[].offered_atstring (RFC 3339) or null- When they were last emailed about a free place. null if they never have been.
waitlist[].took_atstring (RFC 3339) or null- When they took a place from the waitlist. null until they have.
waitlist[].held_untilstring (RFC 3339) or null- When the place offered to them by hand stops being held for them. null unless they have been offered a place by hand.
waitlist[].anonymized_atstring (RFC 3339) or null- When the person on this entry was anonymized, null while they are still on it. An anonymized entry keeps its reference, its list and its state and has no name or email address.
counts.waitingintegercounts.offeredintegercounts.tookintegercounts.leftinteger
Responses
200The waitlist and its counts.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422An unknownstateorkind, aticket_type_idorsession_idthat is not a number, both of them at once,session_idwith kind ticket, orticket_type_idwith kind workshop (invalid_query).500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/waitlist \
-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/waitlist", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"waitlist": [
{
"id": 12,
"ref": "V6EWNE",
"name": "Aoife Byrne",
"email": "aoife@example.com",
"ticket_type_id": 8,
"ticket_type_name": "Early Bird",
"session_id": null,
"session_name": null,
"offering_id": null,
"offering_name": null,
"ticket_ref": null,
"state": "waiting",
"source": "page",
"added_by": "",
"joined_at": "2026-09-23T10:15:00Z",
"left_at": null,
"position": 1,
"locale": "en",
"offered_at": null,
"took_at": null,
"order_id": null,
"order_ref": null,
"held_until": null,
"offered_by": "",
"anonymized_at": "2026-10-14T09:00:00Z"
}
],
"counts": {
"waiting": 14,
"offered": 0,
"took": 0,
"left": 2
}
}
Add someone to the waitlist
POST /events/{slug}/waitlist
Puts someone on a ticket type's waitlist by hand, at the back of the queue. A workshop option's waitlist can't be added to by hand: move them with POST /events/{slug}/tickets/{ref}/reschedule instead. The ticket type must be on this event, not archived, and have its waitlist on. Answers are optional. Any answer to a question the waitlist does not ask is dropped. Set notify to email them to say they're on the waitlist. Recorded in the event's activity against the key.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
Request body
namestring requiredemailstring requiredanswersobject- Answers keyed by question id. Optional. Each answer must be one the question can hold. A yes/no question takes "yes" or "no". A choice takes one of its options, and a multiple choice takes its options separated by ", ". A phone number is written in international form and stored that way. A number must be a number and a date must be YYYY-MM-DD. A country question takes a two-letter country code ("IE"), one its list offers; a country name is refused. Blank answers are left out.
Response fields
idintegernamestringemailstringticket_type_idinteger or null- The ticket type whose waitlist they are on. null when they are waiting for a workshop option.
ticket_type_namestring or null- That ticket type's name. null when they are waiting for a workshop option.
session_idinteger or null- The workshop option whose waitlist they are on. null when they are waiting for a ticket type.
session_namestring or null- That option's name, or "" for an option with no name of its own. null when they are waiting for a ticket type.
offering_idinteger or null- The workshop offering the option belongs to. null when they are waiting for a ticket type.
ticket_refstring or null- The reference of the ticket that is waiting for the workshop option. null once the ticket goes to someone else or its workshop place is refunded, and when they are waiting for a ticket type.
statestring- waiting, offered (emailed about a free place, or offered one by hand, see held_until), took or left.
- one of
waitingofferedtookleft sourcestring- page when they joined from the event page, organizer when someone added them by hand, ticket when they joined a workshop option's waitlist from their ticket.
- one of
pageorganizerticket joined_atstring (RFC 3339)positioninteger or null- Their place in the queue for this ticket type or workshop option, counting only people waiting. null unless they are waiting. For a workshop option, also null once the ticket goes to someone else or its workshop place is refunded.
offered_atstring (RFC 3339) or null- When they were last emailed about a free place. null if they never have been.
held_untilstring (RFC 3339) or null- When the place offered to them by hand stops being held for them. null unless they have been offered a place by hand.
answersarray of WaitlistAnswer- Their answers to the waitlist's questions. Blank answers are left out. Always empty for a workshop option's waitlist, which asks none.
answers[].field_idintegeranswers[].labelstringanswers[].valuestringanonymized_atstring (RFC 3339) or null- When the person on this entry was anonymized, null while they are still on it. An anonymized entry keeps its reference, its list and its state and has no name or email address.
Responses
201They're on the waitlist.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409That email address is already on this ticket type's waitlist (already_waiting). Nothing changed.422A blank name, an email that isn't an address, a ticket type nobody can be added to, or an answer its question can't hold (invalid_body). Nothing is written.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/waitlist \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Niamh Reilly","email":"niamh@example.com","ticket_type_id":8}'
// 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/waitlist", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Niamh Reilly",
"email": "niamh@example.com",
"ticket_type_id": 8
}),
});
const data = await res.json();
{
"id": 12,
"ref": "V6EWNE",
"name": "Aoife Byrne",
"email": "aoife@example.com",
"ticket_type_id": 8,
"ticket_type_name": "Early Bird",
"session_id": null,
"session_name": null,
"offering_id": null,
"offering_name": null,
"ticket_ref": null,
"state": "waiting",
"source": "page",
"added_by": "",
"joined_at": "2026-09-23T10:15:00Z",
"left_at": null,
"position": 1,
"locale": "en",
"offered_at": null,
"took_at": null,
"order_id": null,
"order_ref": null,
"held_until": null,
"offered_by": "",
"answers": [
{
"field_id": 31,
"key": "dietary_needs",
"label": "Dietary needs",
"value": "Vegan"
}
],
"anonymized_at": "2026-10-14T09:00:00Z"
}
Offer a place by hand
POST /events/{slug}/waitlist/offer
Offers a place to people on a waitlist, and emails each of them. Name the people by ids, or ask for the next people waiting for a ticket_type_id, in the order they joined. Everyone named must be waiting for the same ticket type. Each place is held for that person for hold_hours (48 unless you choose 12, 24, 72 or 168), rounded up to the next quarter hour, and nobody else can buy it until then. If they haven't taken it by then, they go back to waiting in their old place.
You can offer more places than are free. Whoever is too late to take theirs is told the place has gone and stays on the list. free_before and over_by say how it stood.
The email is written in each person's own language. Give subject and message together to change its words: they then go to everyone as written. Either one sent exactly as Tito's own words in the event's language still goes in each person's own language. message is the offer's words only. The line saying when the hold ends, the button to take the place and the line to leave the waitlist are always added.
Someone who can't be offered a place as it happens is skipped with the reason: already_held (offered a place by hand already), claim_started (taking a place already), left, took or not_found. Someone offered a place first come first served can be offered one by hand, and the first email is withdrawn. Someone waiting for a workshop option can't be offered a place by hand. Recorded in the event's activity against the key.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
Request body
idsarray of integer- The people to offer a place to, by their waitlist id. All waiting for the same ticket type.
ticket_type_idinteger- With
next, instead ofids: offer a place to the next people waiting for this ticket type. subjectstring- The email's subject. Give it with
message, or neither for the usual words in each person's own language. messagestring- The offer's words. Blank lines separate paragraphs. When the hold ends, the button and the leave line are added under it.
Response fields
skipped[].idintegerfree_beforeinteger or null- How many places were free to offer when the offer was made. null when the ticket type has no limit.
over_byinteger- How many more places were offered than were free. Whoever is too late to take theirs stays on the list.
Responses
200Who was offered a place, and who was skipped.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Noidsand noticket_type_id, both at once,idsthat aren't people on this event's waitlist or are waiting for different ticket types, anextbelow 1, ahold_hoursthat isn't one of the choices, a blanksubjectormessage, or only one of them (invalid_body). A ticket type whose places can't be offered: its waitlist is off, it is a bundle or its time is picked from a timetable, or it isn't on sale on a published event (not_offerable).idsnaming someone waiting for a workshop option: places on a workshop go to whoever takes them first, and to give someone a place you move them from their ticket (workshop_entry). Nothing is written.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/waitlist/offer \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"ids":[1]}'
// 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/waitlist/offer", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"ids": [
1
]
}),
});
const data = await res.json();
{
"offered": [
41,
42
],
"skipped": [
{
"id": 43,
"reason": "already_held"
}
],
"held_until": "2026-09-26T09:15:00Z",
"free_before": 1,
"over_by": 1
}
Get one person on the waitlist
GET /events/{slug}/waitlist/{id}
One person on a ticket type's or a workshop option's waitlist, with their answers to the waitlist's questions.
Requires invitations.view
Path parameters
slugstring required- The event's slug.
idinteger required- The person's id, as the waitlist lists it.
Response fields
idintegernamestringemailstringticket_type_idinteger or null- The ticket type whose waitlist they are on. null when they are waiting for a workshop option.
ticket_type_namestring or null- That ticket type's name. null when they are waiting for a workshop option.
session_idinteger or null- The workshop option whose waitlist they are on. null when they are waiting for a ticket type.
session_namestring or null- That option's name, or "" for an option with no name of its own. null when they are waiting for a ticket type.
offering_idinteger or null- The workshop offering the option belongs to. null when they are waiting for a ticket type.
ticket_refstring or null- The reference of the ticket that is waiting for the workshop option. null once the ticket goes to someone else or its workshop place is refunded, and when they are waiting for a ticket type.
statestring- waiting, offered (emailed about a free place, or offered one by hand, see held_until), took or left.
- one of
waitingofferedtookleft sourcestring- page when they joined from the event page, organizer when someone added them by hand, ticket when they joined a workshop option's waitlist from their ticket.
- one of
pageorganizerticket joined_atstring (RFC 3339)positioninteger or null- Their place in the queue for this ticket type or workshop option, counting only people waiting. null unless they are waiting. For a workshop option, also null once the ticket goes to someone else or its workshop place is refunded.
offered_atstring (RFC 3339) or null- When they were last emailed about a free place. null if they never have been.
held_untilstring (RFC 3339) or null- When the place offered to them by hand stops being held for them. null unless they have been offered a place by hand.
answersarray of WaitlistAnswer- Their answers to the waitlist's questions. Blank answers are left out. Always empty for a workshop option's waitlist, which asks none.
answers[].field_idintegeranswers[].labelstringanswers[].valuestringanonymized_atstring (RFC 3339) or null- When the person on this entry was anonymized, null while they are still on it. An anonymized entry keeps its reference, its list and its state and has no name or email address.
Responses
200The person.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/waitlist/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/waitlist/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 12,
"ref": "V6EWNE",
"name": "Aoife Byrne",
"email": "aoife@example.com",
"ticket_type_id": 8,
"ticket_type_name": "Early Bird",
"session_id": null,
"session_name": null,
"offering_id": null,
"offering_name": null,
"ticket_ref": null,
"state": "waiting",
"source": "page",
"added_by": "",
"joined_at": "2026-09-23T10:15:00Z",
"left_at": null,
"position": 1,
"locale": "en",
"offered_at": null,
"took_at": null,
"order_id": null,
"order_ref": null,
"held_until": null,
"offered_by": "",
"answers": [
{
"field_id": 31,
"key": "dietary_needs",
"label": "Dietary needs",
"value": "Vegan"
}
],
"anonymized_at": "2026-10-14T09:00:00Z"
}
Anonymize a person on the waitlist
POST /events/{slug}/waitlist/{id}/anonymize
Remove the person from one waitlist entry: their name and email address and every answer they gave. Somebody still waiting or offered a place leaves the list (state becomes left) and is offered nothing more. The order a place was taken with, and the ticket a workshop entry joined from, are not changed and are named in kept.order and kept.ticket; anonymize them with POST /orders/{ref}/anonymize and POST /tickets/{ref}/anonymize. What stays: the entry itself with its reference, its list, its state and when they joined. It happens at once and cannot be undone, the API's copy of the admin's Anonymize confirm step, which runs the same code. Copies of the person in sent emails, the record's history, its webhook deliveries, workflow runs and the API request log are cleared too, and their email address comes off every event's mailing list and out of Contacts. Every call writes a line to the data-protection log (GET /data-protection-log) that names the record's reference and never the person. Send no request body; any body is ignored.
Requires erasure
Path parameters
slugstring required- The event's slug.
idinteger required- The person's id, as the waitlist lists it.
Response fields
refstring- The record's reference: a ticket, order, customer, waitlist entry or ticket request ref, or the invitation's id.
anonymized_atstring (RFC 3339)removedobjectremoved.emailsinteger- Sent emails whose address and words were removed. The record that each was sent stays.
removed.link_tagsinteger- Orders whose link tags were removed: every tag that isn't a
utm_*campaign tag, ad click ids included. removed.interestboolean- Their email address was taken off the mailing list, where they signed up to hear when tickets go on sale.
removed.cardboolean- A card saved with a ticket request this withdrew, which Stripe was asked to delete.
keptobjectkept.other_recordsinteger- Other orders, tickets and invitations with the same email address, which are not changed.
kept.customerstring or null- The customer record an anonymized order is linked to, which is not changed.
kept.campaign_tagsinteger- Orders whose
utm_*campaign tags were kept, so they still count toward the link that sold them. kept.orderstring or null- The order a waitlist place was taken with, or that a ticket request's invitation was claimed with, which is not changed.
kept.invitationinteger or null- The invitation a ticket request was approved into, which is not changed.
outside_titoobjectoutside_tito.payment_providerstring or null- The provider that keeps its own record of the payment, with the buyer's name and email address.
- one of
StripePayPal<nil> outside_tito.servicesarray of string- Connected services the person's details were sent to, which keep their own copy.
outside_tito.cardboolean- A card saved with a ticket request that was already decided, which Stripe keeps.
Responses
200The person on the waitlist is anonymized.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The person on this record has already been anonymized (already_anonymized). Nothing changes.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/waitlist/1/anonymize \
-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/waitlist/1/anonymize", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"anonymized": true,
"kind": "ticket",
"ref": "H7K2PX",
"anonymized_at": "2026-10-14T09:00:00Z",
"removed": {
"tickets": 1,
"orders": 1,
"quotes": 1,
"answers": 1,
"files": 1,
"emails": 1,
"contact": true,
"waitlist": true,
"ticket_request": true,
"link_tags": 1,
"interest": true,
"card": true
},
"kept": {
"documents": 1,
"other_records": 1,
"customer": "example",
"campaign_tags": 1,
"order": "H7K2PX",
"ticket": "T9QW4M",
"invitation": 1
},
"outside_tito": {
"payment_provider": "Stripe",
"services": [
"example"
],
"webhooks": true,
"card": true
}
}
Remove someone from the waitlist
POST /events/{slug}/waitlist/{id}/remove
Takes someone off the waitlist. They are not emailed. Removing someone who already left changes nothing and returns them as they are. Someone who took a place can't be removed.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The person's id, as the waitlist lists it.
Response fields
idintegernamestringemailstringticket_type_idinteger or null- The ticket type whose waitlist they are on. null when they are waiting for a workshop option.
ticket_type_namestring or null- That ticket type's name. null when they are waiting for a workshop option.
session_idinteger or null- The workshop option whose waitlist they are on. null when they are waiting for a ticket type.
session_namestring or null- That option's name, or "" for an option with no name of its own. null when they are waiting for a ticket type.
offering_idinteger or null- The workshop offering the option belongs to. null when they are waiting for a ticket type.
ticket_refstring or null- The reference of the ticket that is waiting for the workshop option. null once the ticket goes to someone else or its workshop place is refunded, and when they are waiting for a ticket type.
statestring- waiting, offered (emailed about a free place, or offered one by hand, see held_until), took or left.
- one of
waitingofferedtookleft sourcestring- page when they joined from the event page, organizer when someone added them by hand, ticket when they joined a workshop option's waitlist from their ticket.
- one of
pageorganizerticket joined_atstring (RFC 3339)positioninteger or null- Their place in the queue for this ticket type or workshop option, counting only people waiting. null unless they are waiting. For a workshop option, also null once the ticket goes to someone else or its workshop place is refunded.
offered_atstring (RFC 3339) or null- When they were last emailed about a free place. null if they never have been.
held_untilstring (RFC 3339) or null- When the place offered to them by hand stops being held for them. null unless they have been offered a place by hand.
answersarray of WaitlistAnswer- Their answers to the waitlist's questions. Blank answers are left out. Always empty for a workshop option's waitlist, which asks none.
answers[].field_idintegeranswers[].labelstringanswers[].valuestringanonymized_atstring (RFC 3339) or null- When the person on this entry was anonymized, null while they are still on it. An anonymized entry keeps its reference, its list and its state and has no name or email address.
Responses
200The person, now left.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409They took a place (took).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/waitlist/1/remove \
-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/waitlist/1/remove", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 12,
"ref": "V6EWNE",
"name": "Aoife Byrne",
"email": "aoife@example.com",
"ticket_type_id": 8,
"ticket_type_name": "Early Bird",
"session_id": null,
"session_name": null,
"offering_id": null,
"offering_name": null,
"ticket_ref": null,
"state": "waiting",
"source": "page",
"added_by": "",
"joined_at": "2026-09-23T10:15:00Z",
"left_at": null,
"position": 1,
"locale": "en",
"offered_at": null,
"took_at": null,
"order_id": null,
"order_ref": null,
"held_until": null,
"offered_by": "",
"answers": [
{
"field_id": 31,
"key": "dietary_needs",
"label": "Dietary needs",
"value": "Vegan"
}
],
"anonymized_at": "2026-10-14T09:00:00Z"
}
Withdraw an offer
POST /events/{slug}/waitlist/{id}/withdraw
Takes back a place offered by hand. They go back to waiting in their old place on the list, and they aren't emailed. The link in their email says the place has gone. Recorded in the event's activity against the key.
Requires invitations.manage
Path parameters
slugstring required- The event's slug.
idinteger required- The person's id, as the waitlist lists it.
Response fields
idintegernamestringemailstringticket_type_idinteger or null- The ticket type whose waitlist they are on. null when they are waiting for a workshop option.
ticket_type_namestring or null- That ticket type's name. null when they are waiting for a workshop option.
session_idinteger or null- The workshop option whose waitlist they are on. null when they are waiting for a ticket type.
session_namestring or null- That option's name, or "" for an option with no name of its own. null when they are waiting for a ticket type.
offering_idinteger or null- The workshop offering the option belongs to. null when they are waiting for a ticket type.
ticket_refstring or null- The reference of the ticket that is waiting for the workshop option. null once the ticket goes to someone else or its workshop place is refunded, and when they are waiting for a ticket type.
statestring- waiting, offered (emailed about a free place, or offered one by hand, see held_until), took or left.
- one of
waitingofferedtookleft sourcestring- page when they joined from the event page, organizer when someone added them by hand, ticket when they joined a workshop option's waitlist from their ticket.
- one of
pageorganizerticket joined_atstring (RFC 3339)positioninteger or null- Their place in the queue for this ticket type or workshop option, counting only people waiting. null unless they are waiting. For a workshop option, also null once the ticket goes to someone else or its workshop place is refunded.
offered_atstring (RFC 3339) or null- When they were last emailed about a free place. null if they never have been.
held_untilstring (RFC 3339) or null- When the place offered to them by hand stops being held for them. null unless they have been offered a place by hand.
answersarray of WaitlistAnswer- Their answers to the waitlist's questions. Blank answers are left out. Always empty for a workshop option's waitlist, which asks none.
answers[].field_idintegeranswers[].labelstringanswers[].valuestringanonymized_atstring (RFC 3339) or null- When the person on this entry was anonymized, null while they are still on it. An anonymized entry keeps its reference, its list and its state and has no name or email address.
Responses
200The person, waiting again.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409They haven't been offered a place by hand (not_held), or they have started taking it (claim_started): cancel their order if you need the place back. Nothing changed.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/waitlist/1/withdraw \
-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/waitlist/1/withdraw", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 12,
"ref": "V6EWNE",
"name": "Aoife Byrne",
"email": "aoife@example.com",
"ticket_type_id": 8,
"ticket_type_name": "Early Bird",
"session_id": null,
"session_name": null,
"offering_id": null,
"offering_name": null,
"ticket_ref": null,
"state": "waiting",
"source": "page",
"added_by": "",
"joined_at": "2026-09-23T10:15:00Z",
"left_at": null,
"position": 1,
"locale": "en",
"offered_at": null,
"took_at": null,
"order_id": null,
"order_ref": null,
"held_until": null,
"offered_by": "",
"answers": [
{
"field_id": 31,
"key": "dietary_needs",
"label": "Dietary needs",
"value": "Vegan"
}
],
"anonymized_at": "2026-10-14T09:00:00Z"
}
Reports
Read coupon usage
GET /events/{slug}/reports/coupons
Counts pending and paid orders in one currency. Total orders are distinct across codes. Retired codes with no current usage are omitted. Money is integer minor units. Needs events.view as well as financials.view.
Requires financials.view
Path parameters
slugstring required- The event's slug.
Query parameters
currencystring- One enabled event currency; defaults to the event currency.
Response fields
currencystringcodesarray of objectcodes[].codestringcodes[].labelstringcodes[].coupon_namestringcodes[].code_idintegercodes[].coupon_idintegercodes[].max_redemptionsintegercodes[].ordersintegercodes[].amount_centsintegercodes[].coupon_deletedbooleancodes[].retiredbooleancodes[].percent_bpsintegercodes[].amounts_offobjectcodes[].ticket_type_idsarray of integertotalsobjecttotals.ordersintegertotals.amount_centsinteger
Responses
200Operation completed.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Currency is not enabled.500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/reports/coupons \
-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/coupons", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"currency": "eur",
"codes": [
{
"code": "EARLYBIRD",
"label": "example",
"coupon_name": "Ada Lovelace",
"code_id": 1,
"coupon_id": 1,
"max_redemptions": 1,
"orders": 1,
"amount_cents": 14900,
"coupon_deleted": true,
"retired": true,
"percent_bps": 1,
"amounts_off": {},
"applicability": "ticket",
"ticket_type_ids": [
1
]
}
],
"totals": {
"orders": 1,
"amount_cents": 14900
}
}
Design
The event design
GET /events/{slug}/design
Returns this event’s draft and published design tokens, including look_base, its selected look. Unset tokens fall back to the account design.
Requires events.manage
Path parameters
slugstring required- The event slug.
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The design.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/design \
-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/design", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
Stage event design changes
PATCH /events/{slug}/design
Edits the event design draft using the same token vocabulary and validation as /design. This changes no published page until the event design draft is published. The layout tokens, layout_header, layout_date, layout_tickets, layout_total and layout_ticket, are among them.
Requires events.manage
Path parameters
slugstring required- The event slug.
Request body
tokensobject- Values, keyed as
GET /designlists them. Each is validated by its kind, in the same words the studio would use. Abooltoken takes "1" or "0", or "true" or "false", which are stored as "1" and "0". resetarray of string- Tokens to take back to the layer below — the look's, or the product default. A list of its own rather than a null in
tokens, because for theurlandimagekinds an empty string is a REAL value ("no image"), so clearing and unsetting are two different acts.
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The design, with the staged values in place.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422A token this build does not have, or a value its kind refuses — a bad hex, a radius out of range, a choice outside its set (invalid_body). The message names the token.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/design \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"tokens":null}'
// 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/design", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"tokens": null
}),
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
Read the event stylesheet
GET /events/{slug}/design/css
Returns only this scope’s own CSS, not inherited CSS.
Requires events.manage
Path parameters
slugstring required- The event slug.
Response fields
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
Responses
200The draft and published stylesheet for this scope.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/design/css \
-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/design/css", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
Set the event stylesheet
PUT /events/{slug}/design/css
Saves only to the shared Customize draft. Publish through the event design route to make it public. The stylesheet is served as written on the account’s own address, and on the root domain with anything that loads from another address blocked; draft_blocked and published_blocked say what.
Requires events.manage
Path parameters
slugstring required- The event slug.
Request body
Response fields
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
Responses
200The draft and published stylesheet for this scope.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid or oversized CSS body.500Internal error.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/events/annual-conf/design/css \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"css":"example"}'
// 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/design/css", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"css": "example"
}),
});
const data = await res.json();
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
Discard the staged event design
POST /events/{slug}/design/discard
Discards the entire event draft, including other Customize changes staged in the UI. Published pages stay unchanged.
Requires events.manage
Path parameters
slugstring required- The event slug.
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The design, back to what is published.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/design/discard \
-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/design/discard", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
Read the event documents’ stylesheet
GET /events/{slug}/design/document-css
Returns only this scope’s own CSS, not inherited CSS.
Requires events.manage
Path parameters
slugstring required- The event slug.
Response fields
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
Responses
200The draft and published stylesheet for this scope.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/design/document-css \
-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/design/document-css", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
Set the event documents’ stylesheet
PUT /events/{slug}/design/document-css
Saves only to the shared Customize draft. Publish through the event design route to make it public. The documents’ stylesheet reaches the invoice, the receipt, the refund receipt and the printed ticket, and nothing else; the pages’ stylesheet never reaches them. On a document, anything that loads from another address is left out on every host, and draft_blocked and published_blocked say what.
Requires events.manage
Path parameters
slugstring required- The event slug.
Request body
Response fields
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
Responses
200The draft and published stylesheet for this scope.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid or oversized CSS body.500Internal error.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/events/annual-conf/design/document-css \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"css":"example"}'
// 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/design/document-css", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"css": "example"
}),
});
const data = await res.json();
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
Read the event emails’ stylesheet
GET /events/{slug}/design/email-css
Returns only this scope’s own CSS, not inherited CSS.
Requires events.manage
Path parameters
slugstring required- The event slug.
Response fields
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
Responses
200The draft and published stylesheet for this scope.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/design/email-css \
-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/design/email-css", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
Set the event emails’ stylesheet
PUT /events/{slug}/design/email-css
Saves only to the shared Customize draft. Publish through the event design route to make it public. The emails’ stylesheet reaches every email sent about an event, and nothing else; the pages’ stylesheet never reaches an email. In an email only rules that name one of the email classes (email-card, email-button and the rest, listed in the CSS contract) are kept, every declaration is made !important so it wins over the email’s inline styles, and anything that loads from another address is left out. draft_blocked and published_blocked say what was left out for loading.
Requires events.manage
Path parameters
slugstring required- The event slug.
Request body
Response fields
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
Responses
200The draft and published stylesheet for this scope.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid or oversized CSS body.500Internal error.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/events/annual-conf/design/email-css \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"css":"example"}'
// 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/design/email-css", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"css": "example"
}),
});
const data = await res.json();
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
Start this event's look from your own website
POST /events/{slug}/design/from-site
Reads the website's home page and up to four of its stylesheets, and writes a draft from it into the event's design draft: Paper's settings, with the site's colors, the closest of the hosted fonts to its own, its logo (stored as an uploaded image), its header and footer links in site_header and site_footer, and the layout its shape suits. Only public https addresses on the default port are read, each read is capped in size and time, and an account can read 6 websites in 10 minutes. Nothing from the website is stored as markup or CSS. Nothing is published: POST /events/{slug}/design/publish does that.
Requires events.manage
Path parameters
slugstring required- The event slug.
Request body
urlstring required- The website to read: a host, or an address starting https:// (http:// is read as https://).
layoutboolean- Write the layout the site's shape suits:
layout_header,layout_ticketsandlayout_spacingwhere the home page says, and Paper's for the other layout parts. False, or a site whose shape can't be told, leaves the layout as it is. A picture across the whole window brings no artwork through the API.
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The design, with the website's draft in its draft.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The website couldn't be used:site_err_address(not a website address),site_err_private(not a public address),site_err_unreachable,site_err_refused(the site turned the request away),site_err_slow(over 10 seconds),site_err_notpage(not HTML, or over 2 MB),site_err_nothing(nothing usable found), or an unknown field (invalid_body).429More than 6 websites read in 10 minutes (rate_limited).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/design/from-site \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.example.com"}'
// 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/design/from-site", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://www.example.com"
}),
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
Use this event's look for every event
POST /events/{slug}/design/lift
Copies the event's published look into the account's design draft: its colors, type, buttons, logo, favicons, wallet pass design and stylesheet. Tokens that belong to this event alone stay behind: the arrangement, the banner and where the hero sits, the venue, page and email blocks, and the pass's per-event fields and images. A token the event does not set leaves the account's value as it is, and an event with no stylesheet leaves the account's stylesheet alone. The event's own design is unchanged, and nothing is published: POST /design/publish does that.
Requires settings.manage
Path parameters
slugstring required- The event slug.
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The account's design, with the lifted look in its draft.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/design/lift \
-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/design/lift", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
Publish the staged event design
POST /events/{slug}/design/publish
Publishes the entire event draft, including design tokens, wording and widgets staged in the UI. Content placements are not staged: placing a piece shows it to buyers straight away. Clears the draft and purges this event’s public pages. If no draft is open, the published design stays unchanged.
Requires events.manage
Path parameters
slugstring required- The event slug.
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The design, now published.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/design/publish \
-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/design/publish", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
Start this event's look from a theme
POST /events/{slug}/design/theme
Writes the theme's colors, fonts, buttons, cards, headings and decoration into the event's design draft, and records the theme as look_theme. On an event with no banner picture it also sets the banner artwork and where the event's name sits on it; an event whose banner is a picture keeps the picture, its title color and its place. The logo's place and the event's own stylesheet are never changed. Nothing is published: POST /events/{slug}/design/publish does that.
Requires events.manage
Path parameters
slugstring required- The event slug.
Request body
themestring required- The theme to start from: one of the
look_themetoken's options in the design (paper, classic, ledger, gazette, midnight, signal, fieldwork, pop, riso, afterglow, arcade, sprinkles). Starting from one writes its layout tokens too.
Response fields
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
Responses
200The design, with the theme in its draft.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422A theme this build does not have, or an unknown field (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/design/theme \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"theme":"riso"}'
// 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/design/theme", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"theme": "riso"
}),
});
const data = await res.json();
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
get event wording
GET /events/{slug}/design/wording
The words on this event's pages that can be changed, in one language (locale, en by default): each line's key, the screens it appears on, and its draft and published text. With audience, the lines that audience can word for itself, and its own draft and published text for each, which its buyers see on top of everyone's wording. With terms, the id of a set of payment terms (GET /payment-terms), the same view for those terms: the lines of the invoice emails they can word for themselves, and their own text for each, which an invoice issued under those terms uses on top of everyone's. One of audience or terms, never both.
Requires events.manage
Path parameters
slugstring required
Query parameters
localestringaudiencestring- An audience slug of this event. Lists that audience's own wording rather than everyone's. Not with
terms. termsinteger- The id of a set of payment terms (
GET /payment-terms). Lists those terms' own wording rather than everyone's;0or absent is everyone's. Not withaudience.
Response fields
localestringaudiencestring- The audience slug whose own wording
draftandpublishedcarry, or an empty string for everyone's wording. termsinteger- The id of the payment terms whose own wording
draftandpublishedcarry, or 0 for everyone's wording. Never set beside a non-emptyaudience. draft_openbooleanwordsarray of DesignWordingRowwords[].keystringwords[].screensarray of stringwords[].formsarray of stringwords[].hideablebooleanwords[].formattableboolean- The line may carry bold, italic, bullets and links. Once formatted, its draft and published values are a composer body rather than a plain sentence.
words[].per_audienceboolean- An audience of the event can have its own wording for this line, on top of everyone's. Only these lines are listed with
?audience=, and only these acceptaudiencein a change. words[].per_termsboolean- A set of payment terms can have its own wording for this line, on top of everyone's: the lines of the invoice emails. Only these lines are listed with
?terms=, and only these accepttermsin a change. No line is bothper_audienceandper_terms. words[].draftobjectwords[].publishedobjectwords[].argsarray of string
Responses
200The wording for this scope and locale.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid locale (invalid_locale), an audience that is not one of this event's (invalid_audience), payment terms that do not exist ortermsthat is not an id (invalid_terms), or bothaudienceandtermson one read (invalid_scope).500Internal error.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/design/wording \
-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/design/wording", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"locale": "example",
"audience": "example",
"terms": 1,
"draft_open": true,
"words": [
{
"key": "example",
"screens": [
"example"
],
"forms": [
"example"
],
"hideable": true,
"formattable": true,
"per_audience": true,
"per_terms": true,
"draft": {},
"published": {},
"draft_hidden": true,
"published_hidden": true,
"args": [
"example"
]
}
]
}
set event wording
PATCH /events/{slug}/design/wording
Changes one line of this event's wording, in one language, in the event's design draft. With audience, changes that audience's own wording for the line, which its buyers see instead of everyone's; an empty value goes back to everyone's. With terms, the id of a set of payment terms, changes those terms' own wording for the line, which an invoice issued under those terms uses instead of everyone's; an empty value goes back to everyone's the same way. One of audience or terms, never both. Its pages don't change until the draft is published with POST /events/{slug}/design/publish.
Requires events.manage
Path parameters
slugstring required
Request body
localestring requiredkeystring requiredformstringvaluestring requiredaudiencestring- An event's audience slug: changes that audience's own wording for the line instead of everyone's. The line must be one with
per_audiencetrue. Not accepted for account wording, and not withterms. termsinteger- The id of a set of payment terms (
GET /payment-terms): changes those terms' own wording for the line instead of everyone's. The line must be one withper_termstrue. Not accepted for account wording, and not withaudience.
Response fields
localestringaudiencestring- The audience slug whose own wording
draftandpublishedcarry, or an empty string for everyone's wording. termsinteger- The id of the payment terms whose own wording
draftandpublishedcarry, or 0 for everyone's wording. Never set beside a non-emptyaudience. draft_openbooleanwordsarray of DesignWordingRowwords[].keystringwords[].screensarray of stringwords[].formsarray of stringwords[].hideablebooleanwords[].formattableboolean- The line may carry bold, italic, bullets and links. Once formatted, its draft and published values are a composer body rather than a plain sentence.
words[].per_audienceboolean- An audience of the event can have its own wording for this line, on top of everyone's. Only these lines are listed with
?audience=, and only these acceptaudiencein a change. words[].per_termsboolean- A set of payment terms can have its own wording for this line, on top of everyone's: the lines of the invoice emails. Only these lines are listed with
?terms=, and only these accepttermsin a change. No line is bothper_audienceandper_terms. words[].draftobjectwords[].publishedobjectwords[].argsarray of string
Responses
200The wording for this scope and locale.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid wording input (invalid_body, including a line that is the same for every audience or the same for every payment terms, and a key spelling an audience or terms into itself), an audience that is not one of this event's (invalid_audience), payment terms that do not exist (invalid_terms), or bothaudienceandtermsin one change (invalid_scope).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/design/wording \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"locale":"example","key":"example","value":"example"}'
// 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/design/wording", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"locale": "example",
"key": "example",
"value": "example"
}),
});
const data = await res.json();
{
"locale": "example",
"audience": "example",
"terms": 1,
"draft_open": true,
"words": [
{
"key": "example",
"screens": [
"example"
],
"forms": [
"example"
],
"hideable": true,
"formattable": true,
"per_audience": true,
"per_terms": true,
"draft": {},
"published": {},
"draft_hidden": true,
"published_hidden": true,
"args": [
"example"
]
}
]
}
set event wording visibility
PATCH /events/{slug}/design/wording/visibility
Hides or shows one line of this event's wording, where the line can be hidden, in the event's design draft. Publish the draft with POST /events/{slug}/design/publish.
Requires events.manage
Path parameters
slugstring required
Query parameters
localestring
Request body
keystring required
Response fields
localestringaudiencestring- The audience slug whose own wording
draftandpublishedcarry, or an empty string for everyone's wording. termsinteger- The id of the payment terms whose own wording
draftandpublishedcarry, or 0 for everyone's wording. Never set beside a non-emptyaudience. draft_openbooleanwordsarray of DesignWordingRowwords[].keystringwords[].screensarray of stringwords[].formsarray of stringwords[].hideablebooleanwords[].formattableboolean- The line may carry bold, italic, bullets and links. Once formatted, its draft and published values are a composer body rather than a plain sentence.
words[].per_audienceboolean- An audience of the event can have its own wording for this line, on top of everyone's. Only these lines are listed with
?audience=, and only these acceptaudiencein a change. words[].per_termsboolean- A set of payment terms can have its own wording for this line, on top of everyone's: the lines of the invoice emails. Only these lines are listed with
?terms=, and only these accepttermsin a change. No line is bothper_audienceandper_terms. words[].draftobjectwords[].publishedobjectwords[].argsarray of string
Responses
200The wording for this scope and locale.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid locale or visibility input. Whether a line is shown is the same for every audience and every payment terms, soaudienceandtermsare refused here, as a query parameter or in the body.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/design/wording/visibility \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"key":"example","hidden":true}'
// 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/design/wording/visibility", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"key": "example",
"hidden": true
}),
});
const data = await res.json();
{
"locale": "example",
"audience": "example",
"terms": 1,
"draft_open": true,
"words": [
{
"key": "example",
"screens": [
"example"
],
"forms": [
"example"
],
"hideable": true,
"formattable": true,
"per_audience": true,
"per_terms": true,
"draft": {},
"published": {},
"draft_hidden": true,
"published_hidden": true,
"args": [
"example"
]
}
]
}
Settings
List event provider connections
GET /events/{slug}/connections
Lists only providers whose credentials belong to this event. Status contains no provider fields or sealed secrets. Connection request and response bodies are omitted from API logs. Credentials are never returned.
Requires settings.manage
Path parameters
slugstring required
Response fields
connectionsarray of Connectionconnections[].categorystring- What kind of service it is, from the relay's manifest — "payments", "networking", "lead-retrieval" and so on.
connections[].kindstring- How this service is connected:
oauth,pat(a pasted key),both, orpartner_referral. Use the corresponding credential or authorization API route for this provider kind. connections[].connectedbooleanconnections[].connected_atstring (RFC 3339)connections[].stripe_accountsarray of StripeAccount- Stripe only: every Stripe account this account holds, the first connected first.
account_idis the default among them, the one new events pay into. Absent for every other service. connections[].stripe_accounts[].idstringconnections[].stripe_accounts[].namestring- What Stripe calls the account, empty when it has no name.
connections[].account_idstring- The provider-side identifier worth showing: a Stripe account or a PayPal merchant id. Not a credential — it is on the organizer's own screen and in the provider's dashboard — and empty for a service that has neither.
connections[].sheetobject- The spreadsheet a Google Sheets connection writes to, null until one has been made. The id is not a secret: it is in the address bar of anyone who opens the sheet.
connections[].sheet.idstringconnections[].sheet.urlstringconnections[].livemodeboolean- On a connected Stripe row: true for a live Stripe account, which takes real payments, and false for a Stripe sandbox, which takes test payments only. Null on every other row, and on a Stripe row that is not connected.
Responses
200Connection result401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.503The relay provider catalog is unavailable
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/connections \
-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/connections", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"connections": [
{
"id": "stripe",
"name": "Stripe",
"category": "payments",
"kind": "oauth",
"connected": true,
"connected_at": "2026-09-01T10:04:00Z",
"stripe_accounts": [
{
"id": "acct_1MEXAMPLE",
"name": "Example Events Ltd",
"livemode": true,
"default": true,
"events": 4
}
],
"account_id": "acct_1MEXAMPLE",
"sheet": {
"id": "1AbCdEf",
"url": "https://docs.google.com/spreadsheets/d/1AbCdEf/edit"
},
"livemode": true
}
]
}
Store a provider credential
POST /events/{slug}/connections/{service}/credentials
Stores a trimmed key of 1 to 4096 bytes through the relay. The provider must accept pasted keys in this scope. Stored does not mean the provider has verified the key. Successful responses have Cache-Control: no-store. Provider failures are logged without credential bodies. Connection request and response bodies are omitted from API logs. Credentials are never returned.
Requires settings.manage
Path parameters
slugstring requiredservicestring required
Request body
keystring required
Response fields
servicestring
Responses
200Connection result401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid input, provider kind or provider unavailable in this environment500Internal error.502The relay could not complete the request503The relay provider catalog is unavailable
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/connections/{service}/credentials \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"key":"example"}'
// 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/connections/{service}/credentials", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"key": "example"
}),
});
const data = await res.json();
{
"service": "example",
"status": "stored"
}
Remove an event provider credential
POST /events/{slug}/connections/{service}/disconnect
Removes the local credential for a provider with a pasted-key option. Other event and account credentials remain. Revoke the original key at the provider if needed. Repeated removal returns 204. Providers without pasted-key connections return 422. Connection request and response bodies are omitted from API logs. Credentials are never returned.
Requires settings.manage
Path parameters
slugstring requiredservicestring required
Request body
Responses
204The event credential was removed401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid input or provider kind500Internal error.503The relay provider catalog is unavailable
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/connections/{service}/disconnect \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"confirmed":true}'
// 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/connections/{service}/disconnect", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"confirmed": true
}),
});
const data = await res.json();
List event integration targets
GET /events/{slug}/integrations
Lists supported sync adapters, connection state, saved settings, and ticket-type mappings. Credentials are never returned.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
integrationsarray of EventSyncTargetintegrations[].servicestringintegrations[].namestringintegrations[].connectedbooleanintegrations[].settingsobject or nullintegrations[].schemaarray of objectintegrations[].schema[].keystringintegrations[].schema[].requiredbooleanintegrations[].type_mapobject
Responses
200Integration settings or choices.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/integrations \
-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/integrations", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"integrations": [
{
"service": "example",
"name": "Ada Lovelace",
"connected": true,
"settings": {},
"schema": [
{
"key": "example",
"kind": "text",
"required": true
}
],
"type_map": {}
}
]
}
Remove an event integration target
DELETE /events/{slug}/integrations/{service}
Removes settings, ticket-type mappings, and setup history in one transaction. Leaves the account connection and remote account intact.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
servicestring required
Responses
204Removed.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/integrations/{service} \
-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/integrations/{service}", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
Read an event integration target
GET /events/{slug}/integrations/{service}
Settings are null until configured. Credentials are never returned.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
servicestring required
Response fields
servicestringnamestringconnectedbooleansettingsobject or nullschemaarray of objectschema[].keystringschema[].requiredbooleantype_mapobject
Responses
200Integration settings or choices.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/integrations/{service} \
-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/integrations/{service}", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"service": "example",
"name": "Ada Lovelace",
"connected": true,
"settings": {},
"schema": [
{
"key": "example",
"kind": "text",
"required": true
}
],
"type_map": {}
}
Save event integration settings
PUT /events/{slug}/integrations/{service}
Replaces declared settings after provider verification. Required text settings must be nonblank, at most 4096 bytes; booleans must be JSON booleans and default to false when omitted. Unknown keys are rejected. A failed verification leaves saved settings unchanged. The ticket-type map is preserved.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
servicestring required
Request body
settingsobject required
Response fields
servicestringnamestringconnectedbooleansettingsobject or nullschemaarray of objectschema[].keystringschema[].requiredbooleantype_mapobject
Responses
200Integration settings or choices.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The account is not connected to this service, or the target is not configured.422Invalid settings or provider refusal.500Internal error.502Temporary verification failure.503Connection service unavailable.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/events/annual-conf/integrations/{service} \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"settings":null}'
// 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/integrations/{service}", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"settings": null
}),
});
const data = await res.json();
{
"service": "example",
"name": "Ada Lovelace",
"connected": true,
"settings": {},
"schema": [
{
"key": "example",
"kind": "text",
"required": true
}
],
"type_map": {}
}
List integration options
GET /events/{slug}/integrations/{service}/options
Lists provider choices for one choice setting, using the account connection. No saved target is needed.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
servicestring required
Query parameters
settingstring required
Response fields
Responses
200Integration settings or choices.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The account is not connected to this service.422Unknown choice setting.500Internal error.502Provider listing failed.503Connection service unavailable.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/integrations/{service}/options \
-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/integrations/{service}/options", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"options": [
{
"id": "example",
"name": "Ada Lovelace"
}
]
}
Replace integration ticket-type mappings
PUT /events/{slug}/integrations/{service}/type-map
Replaces the entire map. Empty object clears it; empty remote IDs mean do not send. Local IDs must belong to this event and remote IDs must occur in the provider listing. Provider listing failure leaves the existing map unchanged. For Swapcard this is the only way to set the map. Brella and Mailchimp also have a Set up screen in the admin that writes it.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
servicestring required
Request body
type_mapobject required
Response fields
servicestringnamestringconnectedbooleansettingsobject or nullschemaarray of objectschema[].keystringschema[].requiredbooleantype_mapobject
Responses
200Integration settings or choices.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The account is not connected to this service, or the target is not configured.422Invalid local or remote ticket type.500Internal error.502Provider listing failed.503Connection service unavailable.
curl -X PUT https://<yourslug>.go.tito.io/api/v1/events/annual-conf/integrations/{service}/type-map \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"type_map":null}'
// 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/integrations/{service}/type-map", {
method: "PUT",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"type_map": null
}),
});
const data = await res.json();
{
"service": "example",
"name": "Ada Lovelace",
"connected": true,
"settings": {},
"schema": [
{
"key": "example",
"kind": "text",
"required": true
}
],
"type_map": {}
}
List integration types
GET /events/{slug}/integrations/{service}/types
Lists choices from the provider. The types endpoint needs a configured target.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
servicestring required
Response fields
Responses
200Integration settings or choices.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The account is not connected to this service, or the target is not configured.500Internal error.502Provider listing failed.503Connection service unavailable.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/integrations/{service}/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/integrations/{service}/types", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"types": [
{
"id": "example",
"name": "Ada Lovelace"
}
]
}
Get an event's settings
GET /events/{slug}/settings
The event's settings panels. Each is one statement and one history entry, exactly as the admin's own modal writes it, so a save from a key and a save from a person are the same save. General is not here: PATCH /events/{slug} is that panel. An unreadable value is refused rather than clamped — a form's hand-crafted POST is one thing, a caller's typo is a question it wants answered. payment is null unless the key carries payment.manage, the split the admin's own sidebar makes for owners and admins.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
registration.sales_end_modestring- "ends" (when the event ends), "never", or "custom" with sales_end_at saying when.
- one of
endsnevercustom registration.sales_end_atstring (RFC 3339) or null- When selling stops; null unless the mode is custom.
registration.field_collection_timingstring- Whether the questions are asked before paying or after.
- one of
before_paymentafter_payment registration.require_holder_namebooleanregistration.require_holder_emailbooleanregistration.require_holder_answersboolean- Whether a ticket the booker gives to somebody else must have its required questions answered before it is sent.
registration.qr_gatestring- When a ticket's QR code appears: always, once it is named, or once its questions are done.
- one of
alwaysnameddone registration.min_tickets_per_orderinteger- The fewest tickets one buyer's order may hold, 0 for no limit. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it.
registration.max_tickets_per_orderinteger- The most tickets one buyer's order may hold, 0 for no limit. Counted as min_tickets_per_order is, and an addition to an order counts what the order already holds.
registration.capacityinteger or null- The most tickets the whole event can sell, across every ticket type, null for no limit. Once the event reaches it every ticket type is off sale, whatever each has left. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it. An organizer's own orders in the admin may go past it.
payment.stripe_accountstring- Which of the account's Stripe accounts this event's new card payments go into: an account id, empty for the account's default, or
nonewhen nobody has picked one and the event takes no card payments. Orders already paid stay with the account they were paid into. payment.checkout_modestring- Where buyers pay: hosted (Stripe's own page, the zero-JS baseline) or custom (on the event's own pages, with Stripe's payment fields). Events saved with the retired value embedded read back as custom.
- one of
hostedcustom payment.hold_minutesinteger- How many minutes a buyer's selection is held for them while they fill in their details, 5 to 60. A hold is extended while nobody else is waiting for the tickets.
payment.stripe_customers_enabledbooleanpayment.bank_transfer_enabledbooleanpayment.bank_transfer_reminder_daysstring- Days before the due date to remind, comma-separated. Normalized on write.
payment.bank_transfer_overdue_daysstring- Days after the due date to chase, comma-separated. Normalized on write.
payment.bank_transfer_auto_cancel_daysinteger- Days overdue before the order is released; 0 is off, 365 the ceiling.
payment.paypal_enabledbooleanpayment.express_checkout_enabledboolean- Whether buyers are offered Apple Pay, Google Pay, Link and PayPal as one-tap shortcuts. Card payment stays either way, and PayPal stays where it is the only way to pay. Off here wins over an audience's own
show_express_checkout. payment.extra_currenciesarray of string- Currencies this event sells in besides its own, in the account's order.
after_registering.email_sender_idinteger- The account sender (
GET /email-senders) — the domain — this event's email goes out from, or 0 to follow the account's default. after_registering.email_sender_address_idinteger- The address on that sender's domain this event's email goes out as (an
idfrom the sender'saddresses), or 0 to follow the account's default. after_registering.reply_tostring- Where replies to this event's email go. It wins over the Reply-To of the address the email goes out as, and works with no sending domain at all. "" follows the address: its own Reply-To, or the address itself.
after_registering.receipt_deliverystring- How the order confirmation hands over a paid order's receipt: attached as a PDF, as a link to it, or not at all (the buyer downloads it from their order page).
- one of
attachlinknone after_registering.invoice_details_daysinteger- How many days after each order its buyer may update their own invoice details from their order page, or 0. At most one of this and
invoice_details_untilis set; neither means buyers can't. after_registering.invoice_details_untilstring- Until when buyers may update their own invoice details,
2006-01-02T15:04on the event's clock, or "". after_registering.holder_changes_stoppedboolean- Whether an attendee may still change their own details.
after_registering.holder_changes_messagestring- What an attendee reads when the window is shut; "" means the shipped sentence, in their own language.
after_registering.holder_email_lockedbooleanafter_registering.holder_details_lockedbooleanafter_registering.self_service_changesboolean- Whether a buyer may remove a ticket or cancel from their own order page. Off by default: it moves money.
after_registering.self_service_untilstring- The cutoff for that, "2006-01-02T15:04" on the event's clock, or "".
after_registering.order_qr_code_enabledboolean- One code per order at the door instead of one per ticket.
after_registering.calendar_links_enabledboolean- Whether people are offered Add to calendar after they book: on their order page, their ticket page and in the emails. On by default. The offer on the event page before booking is a separate switch,
calendar_on_event_pageon the event. after_registering.allow_upgradesboolean- Whether buyers can move a ticket to a dearer ticket type from their order page. An upgrade costs the difference between the two ticket prices, whatever the buyer paid. On by default.
after_registering.allow_downgradesboolean- Whether buyers can move a ticket to a cheaper ticket type from their order page, and get the difference back. Nobody gets back more than they paid for their ticket. Off by default.
checkoutobject- An event's Checkout panel: its own answer to the account's price display and company question, and the link to its terms and conditions.
checkout.tax_displaystring- This event's own answer:
incshows prices including tax,excexcluding it,inheritfollows the account. - one of
inheritincexc checkout.tax_display_excludesboolean- The resolved answer, and the one to read: true when this event's public prices are shown excluding tax.
checkout.ask_companystring- This event's own answer to whether the checkout asks if the buyer is booking for a company.
onlets the buyer tick a box,organizationsmakes every booking an organization's with no box to tick,offnever asks, andinheritfollows the account. - one of
inheritonofforganizations checkout.company_fieldsobject- What an
organizationsevent does with each of an organization's details beyond its name, which is always asked and required:hiddenleaves it off the form,optionalasks for it,requiredrefuses the booking without it. Kept but not applied under any otherask_company. On PATCH, the details named are set and the rest keep their state. checkout.terms_urlstring- The organizer's own terms and conditions page, or an empty string for none. Set, the booking form carries a line above the button at its end saying buyers agree to these terms, linked to this address.
checkout.collect_unfinishedboolean- Collect emails from people who don't finish registering. On, the booking form says "We may email you about this event" above its payment button. A buyer who stops after reading it is listed under Abandoned in Orders and can be messaged until the event ends. A buyer who stopped while it was off is never listed. Off by default.
featuresobject- Which sections this event's admin sidebar shows. A preference, never a kill switch: it hides nothing from a buyer and stops no route.
features.in_usearray of string- Sections something is already using; these show no switch and carry no preference.
Responses
200The panels.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/settings \
-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/settings", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"registration": {
"sales_end_mode": "ends",
"sales_end_at": null,
"field_collection_timing": "before_payment",
"require_holder_name": false,
"require_holder_email": false,
"require_holder_answers": false,
"qr_gate": "always",
"quotes_enabled": false,
"quotes_on_main": false,
"min_tickets_per_order": 0,
"max_tickets_per_order": 6,
"capacity": 500,
"quote_audience_ids": [
6
]
},
"payment": {
"payment_expiry_days": 0,
"stripe_account": "acct_1MEXAMPLE",
"checkout_mode": "custom",
"hold_minutes": 15,
"stripe_customers_enabled": false,
"bank_transfer_enabled": false,
"bank_transfer_due_days": 14,
"bank_transfer_reminder_days": "1,3,7",
"bank_transfer_overdue_days": "1,7",
"bank_transfer_auto_cancel_days": 0,
"paypal_enabled": false,
"express_checkout_enabled": true,
"extra_currencies": [
"usd",
"gbp"
],
"currency_assignment": ""
},
"after_registering": {
"email_sender_id": 0,
"email_sender_address_id": 0,
"reply_to": "",
"receipt_delivery": "attach",
"invoice_details_days": 30,
"invoice_details_until": "",
"holder_changes_stopped": false,
"holder_changes_until": "",
"holder_changes_message": "",
"holder_email_locked": false,
"holder_details_locked": false,
"self_service_changes": false,
"self_service_until": "",
"order_qr_code_enabled": false,
"calendar_links_enabled": true,
"allow_upgrades": true,
"allow_downgrades": false
},
"checkout": {
"tax_display": "inherit",
"tax_display_excludes": false,
"ask_company": "inherit",
"company_fields": {
"address": "optional",
"country": "required",
"po": "hidden",
"vat": "required"
},
"asks_company": true,
"terms_url": "https://example.com/terms",
"collect_unfinished": false
},
"features": {
"sections": [
"quotes"
],
"available": [
"quotes",
"orders"
],
"in_use": [
"orders"
]
}
}
Get the after-purchase settings
GET /events/{slug}/settings/after-registering
What a buyer may change once they have paid.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
email_sender_idinteger- The account sender (
GET /email-senders) — the domain — this event's email goes out from, or 0 to follow the account's default. email_sender_address_idinteger- The address on that sender's domain this event's email goes out as (an
idfrom the sender'saddresses), or 0 to follow the account's default. reply_tostring- Where replies to this event's email go. It wins over the Reply-To of the address the email goes out as, and works with no sending domain at all. "" follows the address: its own Reply-To, or the address itself.
receipt_deliverystring- How the order confirmation hands over a paid order's receipt: attached as a PDF, as a link to it, or not at all (the buyer downloads it from their order page).
- one of
attachlinknone invoice_details_daysinteger- How many days after each order its buyer may update their own invoice details from their order page, or 0. At most one of this and
invoice_details_untilis set; neither means buyers can't. invoice_details_untilstring- Until when buyers may update their own invoice details,
2006-01-02T15:04on the event's clock, or "". holder_changes_messagestring- What an attendee reads when the window is shut; "" means the shipped sentence, in their own language.
holder_email_lockedbooleanholder_details_lockedbooleanself_service_changesboolean- Whether a buyer may remove a ticket or cancel from their own order page. Off by default: it moves money.
calendar_links_enabledboolean- Whether people are offered Add to calendar after they book: on their order page, their ticket page and in the emails. On by default. The offer on the event page before booking is a separate switch,
calendar_on_event_pageon the event. allow_upgradesboolean- Whether buyers can move a ticket to a dearer ticket type from their order page. An upgrade costs the difference between the two ticket prices, whatever the buyer paid. On by default.
allow_downgradesboolean- Whether buyers can move a ticket to a cheaper ticket type from their order page, and get the difference back. Nobody gets back more than they paid for their ticket. Off by default.
Responses
200The panel.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/settings/after-registering \
-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/settings/after-registering", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"email_sender_id": 0,
"email_sender_address_id": 0,
"reply_to": "",
"receipt_delivery": "attach",
"invoice_details_days": 30,
"invoice_details_until": "",
"holder_changes_stopped": false,
"holder_changes_until": "",
"holder_changes_message": "",
"holder_email_locked": false,
"holder_details_locked": false,
"self_service_changes": false,
"self_service_until": "",
"order_qr_code_enabled": false,
"calendar_links_enabled": true,
"allow_upgrades": true,
"allow_downgrades": false
}
Update the after-purchase settings
PATCH /events/{slug}/settings/after-registering
self_service_until is validated on write so a malformed value can never reach the fail-closed gate. Sending the shipped closed-changes sentence stores nothing, which is what keeps that note rendering in each attendee's own language.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Request body
email_sender_idinteger- The account sender this event's email goes out from, or 0 to follow the account's default. Must be 0, the event's current sender, or one of the account's
livesenders. Given alone, the event sends as that sender's primary address (itsfrom_local). Given withemail_sender_address_id, it must be that address's sender. email_sender_address_idinteger- The address this event's email goes out as, or 0 to follow the account's default. Must be 0, the event's current address, or an address on one of the account's
livesenders. reply_tostring- Where replies to this event's email go: one plain email address, or "" to follow the address the email goes out as. Anything else is HTTP 422
invalid_body. receipt_deliverystring- How the order confirmation hands over a paid order's receipt: attached as a PDF, as a link to it, or not at all (the buyer downloads it from their order page).
- one of
attachlinknone invoice_details_daysinteger- How many days after each order its buyer may update their own invoice details from their order page, or 0. At most one of this and
invoice_details_untilis set; neither means buyers can't. Setting it above 0 replaces a date window; 0 turns a days window off. invoice_details_untilstring- Until when buyers may update their own invoice details,
2006-01-02T15:04on the event's clock, or "". Setting it replaces a days window; "" turns a date window off. Sending it withinvoice_details_daysabove 0 answers 422. holder_changes_messagestring- What an attendee reads when the window is shut; "" means the shipped sentence, in their own language.
holder_email_lockedbooleanholder_details_lockedbooleanself_service_changesboolean- Whether a buyer may remove a ticket or cancel from their own order page. Off by default: it moves money.
calendar_links_enabledboolean- Whether people are offered Add to calendar after they book: on their order page, their ticket page and in the emails. On by default. The offer on the event page before booking is a separate switch,
calendar_on_event_pageon the event. allow_upgradesboolean- Whether buyers can move a ticket to a dearer ticket type from their order page. An upgrade costs the difference between the two ticket prices, whatever the buyer paid. On by default.
allow_downgradesboolean- Whether buyers can move a ticket to a cheaper ticket type from their order page, and get the difference back. Nobody gets back more than they paid for their ticket. Off by default.
Response fields
email_sender_idinteger- The account sender (
GET /email-senders) — the domain — this event's email goes out from, or 0 to follow the account's default. email_sender_address_idinteger- The address on that sender's domain this event's email goes out as (an
idfrom the sender'saddresses), or 0 to follow the account's default. reply_tostring- Where replies to this event's email go. It wins over the Reply-To of the address the email goes out as, and works with no sending domain at all. "" follows the address: its own Reply-To, or the address itself.
receipt_deliverystring- How the order confirmation hands over a paid order's receipt: attached as a PDF, as a link to it, or not at all (the buyer downloads it from their order page).
- one of
attachlinknone invoice_details_daysinteger- How many days after each order its buyer may update their own invoice details from their order page, or 0. At most one of this and
invoice_details_untilis set; neither means buyers can't. invoice_details_untilstring- Until when buyers may update their own invoice details,
2006-01-02T15:04on the event's clock, or "". holder_changes_messagestring- What an attendee reads when the window is shut; "" means the shipped sentence, in their own language.
holder_email_lockedbooleanholder_details_lockedbooleanself_service_changesboolean- Whether a buyer may remove a ticket or cancel from their own order page. Off by default: it moves money.
calendar_links_enabledboolean- Whether people are offered Add to calendar after they book: on their order page, their ticket page and in the emails. On by default. The offer on the event page before booking is a separate switch,
calendar_on_event_pageon the event. allow_upgradesboolean- Whether buyers can move a ticket to a dearer ticket type from their order page. An upgrade costs the difference between the two ticket prices, whatever the buyer paid. On by default.
allow_downgradesboolean- Whether buyers can move a ticket to a cheaper ticket type from their order page, and get the difference back. Nobody gets back more than they paid for their ticket. Off by default.
Responses
200The panel as it now stands.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 held a value this panel does not know (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/settings/after-registering \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"email_sender_id":0}'
// 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/settings/after-registering", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"email_sender_id": 0
}),
});
const data = await res.json();
{
"email_sender_id": 0,
"email_sender_address_id": 0,
"reply_to": "",
"receipt_delivery": "attach",
"invoice_details_days": 30,
"invoice_details_until": "",
"holder_changes_stopped": false,
"holder_changes_until": "",
"holder_changes_message": "",
"holder_email_locked": false,
"holder_details_locked": false,
"self_service_changes": false,
"self_service_until": "",
"order_qr_code_enabled": false,
"calendar_links_enabled": true,
"allow_upgrades": true,
"allow_downgrades": false
}
Get the automated emails switched off
GET /events/{slug}/settings/automated-emails
Which of the event's automated emails are switched off, and which can be. A switched-off email isn't sent for this event; a send the team asks for by hand still goes.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
Responses
200The switches.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/settings/automated-emails \
-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/settings/automated-emails", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"switched_off": [
"order"
],
"switchable": [
"moved",
"order",
"order_changed",
"request",
"rsvp",
"ticket",
"waitlist_already",
"waitlist_joined"
]
}
Switch automated emails off or on
PATCH /events/{slug}/settings/automated-emails
switched_off is the whole list, replaced: every email not named is switched back on. Only the emails in switchable can be switched off. Sign-in links and codes, payment requests, refund and cancellation notices, quote emails and waitlist offers are always sent, and naming one is refused.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Request body
Response fields
Responses
200The switches as they now stand.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422switched_offwas absent or named an email that can't be switched off (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/settings/automated-emails \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"switched_off":["example"]}'
// 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/settings/automated-emails", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"switched_off": [
"example"
]
}),
});
const data = await res.json();
{
"switched_off": [
"order"
],
"switchable": [
"moved",
"order",
"order_changed",
"request",
"rsvp",
"ticket",
"waitlist_already",
"waitlist_joined"
]
}
Get the event's checkout settings
GET /events/{slug}/settings/checkout
This event's own answer to the account's price display and company question, and what each resolves to. inherit follows the account's GET /checkout.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
tax_displaystring- This event's own answer:
incshows prices including tax,excexcluding it,inheritfollows the account. - one of
inheritincexc tax_display_excludesboolean- The resolved answer, and the one to read: true when this event's public prices are shown excluding tax.
ask_companystring- This event's own answer to whether the checkout asks if the buyer is booking for a company.
onlets the buyer tick a box,organizationsmakes every booking an organization's with no box to tick,offnever asks, andinheritfollows the account. - one of
inheritonofforganizations company_fieldsobject- What an
organizationsevent does with each of an organization's details beyond its name, which is always asked and required:hiddenleaves it off the form,optionalasks for it,requiredrefuses the booking without it. Kept but not applied under any otherask_company. On PATCH, the details named are set and the rest keep their state. terms_urlstring- The organizer's own terms and conditions page, or an empty string for none. Set, the booking form carries a line above the button at its end saying buyers agree to these terms, linked to this address.
collect_unfinishedboolean- Collect emails from people who don't finish registering. On, the booking form says "We may email you about this event" above its payment button. A buyer who stops after reading it is listed under Abandoned in Orders and can be messaged until the event ends. A buyer who stopped while it was off is never listed. Off by default.
Responses
200The panel.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/settings/checkout \
-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/settings/checkout", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"tax_display": "inherit",
"tax_display_excludes": false,
"ask_company": "inherit",
"company_fields": {
"address": "optional",
"country": "required",
"po": "hidden",
"vat": "required"
},
"asks_company": true,
"terms_url": "https://example.com/terms",
"collect_unfinished": false
}
Update the event's checkout settings
PATCH /events/{slug}/settings/checkout
Only the keys present change. inherit hands the answer back to the account. A value this panel does not know is refused. A change that moves what this event's page advertises purges that page.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Request body
tax_displaystring- This event's own answer:
incshows prices including tax,excexcluding it,inheritfollows the account. - one of
inheritincexc ask_companystring- This event's own answer to whether the checkout asks if the buyer is booking for a company.
onlets the buyer tick a box,organizationsmakes every booking an organization's with no box to tick,offnever asks, andinheritfollows the account. - one of
inheritonofforganizations company_fieldsobject- What an
organizationsevent does with each of an organization's details beyond its name, which is always asked and required:hiddenleaves it off the form,optionalasks for it,requiredrefuses the booking without it. Kept but not applied under any otherask_company. On PATCH, the details named are set and the rest keep their state. terms_urlstring- An http or https address, or an empty string to take the link off. A bare domain such as
example.com/termsis read as https. Anything else is refused. collect_unfinishedboolean- Switch collecting emails from people who don't finish registering on or off. Switching it off stops collecting anyone new. People already collected stay until the event ends.
Response fields
tax_displaystring- This event's own answer:
incshows prices including tax,excexcluding it,inheritfollows the account. - one of
inheritincexc tax_display_excludesboolean- The resolved answer, and the one to read: true when this event's public prices are shown excluding tax.
ask_companystring- This event's own answer to whether the checkout asks if the buyer is booking for a company.
onlets the buyer tick a box,organizationsmakes every booking an organization's with no box to tick,offnever asks, andinheritfollows the account. - one of
inheritonofforganizations company_fieldsobject- What an
organizationsevent does with each of an organization's details beyond its name, which is always asked and required:hiddenleaves it off the form,optionalasks for it,requiredrefuses the booking without it. Kept but not applied under any otherask_company. On PATCH, the details named are set and the rest keep their state. terms_urlstring- The organizer's own terms and conditions page, or an empty string for none. Set, the booking form carries a line above the button at its end saying buyers agree to these terms, linked to this address.
collect_unfinishedboolean- Collect emails from people who don't finish registering. On, the booking form says "We may email you about this event" above its payment button. A buyer who stops after reading it is listed under Abandoned in Orders and can be messaged until the event ends. A buyer who stopped while it was off is never listed. Off by default.
Responses
200The panel as it now stands.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 held a value this panel does not know (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/settings/checkout \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"tax_display":"inherit"}'
// 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/settings/checkout", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"tax_display": "inherit"
}),
});
const data = await res.json();
{
"tax_display": "inherit",
"tax_display_excludes": false,
"ask_company": "inherit",
"company_fields": {
"address": "optional",
"country": "required",
"po": "hidden",
"vat": "required"
},
"asks_company": true,
"terms_url": "https://example.com/terms",
"collect_unfinished": false
}
Get the event details
GET /events/{slug}/settings/details
The account's event details and where this event stands on each: its own answer, or the account's default doing the talking.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
Responses
200The details.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/settings/details \
-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/settings/details", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"details": [
{
"field_id": 4,
"key": "sponsor",
"label": "Sponsor",
"kind": "text",
"value": "Globex",
"default": "Acme",
"inherited": false
}
]
}
Update the event details
PATCH /events/{slug}/settings/details
values maps a field id to its answer. A detail this body says nothing about keeps what it had. A JSON null falls back to the account's default — the panel's inherit box, which DELETES the stored row; an empty string is "this event has nothing to say", which stores one. The two read alike today and differ the month the account adds a default. One history entry covers the whole save, keyed by each detail's own key, and a number that is not one refuses the save whole rather than leaving half of it applied.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Request body
valuesobject required- Field id to answer. null falls back to the account's default; "" says this event has nothing to say.
Response fields
Responses
200The details as they now stand.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422valueswas absent, keyed by something that is not a field id, named a detail this account does not have, or gave a number detail something that is not a number (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/settings/details \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"values":null}'
// 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/settings/details", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"values": null
}),
});
const data = await res.json();
{
"details": [
{
"field_id": 4,
"key": "sponsor",
"label": "Sponsor",
"kind": "text",
"value": "Globex",
"default": "Acme",
"inherited": false
}
]
}
Get the sidebar sections
GET /events/{slug}/settings/features
Which sections this event's admin shows, which it offers, and which are in use.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
in_usearray of string- Sections something is already using; these show no switch and carry no preference.
Responses
200The panel.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/settings/features \
-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/settings/features", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"sections": [
"quotes"
],
"available": [
"quotes",
"orders"
],
"in_use": [
"orders"
]
}
Update the sidebar sections
PATCH /events/{slug}/settings/features
sections is the whole list, replaced. This is a sidebar preference and nothing else: it hides nothing from a buyer, stops no route and loses no row. A section already in use shows no switch and carries no preference, so naming one changes nothing; a key the panel does not offer this event is refused.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Request body
Response fields
in_usearray of string- Sections something is already using; these show no switch and carry no preference.
Responses
200The panel as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422sectionswas absent or named a key this event's panel does not offer (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/settings/features \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"sections":["example"]}'
// 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/settings/features", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"sections": [
"example"
]
}),
});
const data = await res.json();
{
"sections": [
"quotes"
],
"available": [
"quotes",
"orders"
],
"in_use": [
"orders"
]
}
Get the invitation settings
GET /events/{slug}/settings/invitations
Every key with the value in force — the account's default standing in where the event says nothing.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
settingsobject- Key to value: the asking rules (off/optional/required), the on/off switches, and the refund rules.
Responses
200The settings.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/settings/invitations \
-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/settings/invitations", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"settings": {
"cancellation": "off",
"primary_email": "required",
"primary_name": "optional",
"refund_mode": "none"
}
}
Update the invitation settings
PATCH /events/{slug}/settings/invitations
A key this body omits keeps what it had, and "inherit" puts one back to the account's default (deleting its row). guests_on and cancellation_on are the panel's authoritative switches: off means off whatever the keys inside them say, and switching cancellation off deliberately KEEPS the refund rules, so turning it back on does not lose them. These are rows on invitation_settings rather than columns on the event, so the history entry is a before/after diff under its own action.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Request body
settingsobject- The keys to change; "inherit" puts one back to the account's default. Omitted keys keep what they had.
Response fields
settingsobject- Key to value: the asking rules (off/optional/required), the on/off switches, and the refund rules.
Responses
200The settings as they now stand.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422A key that is not one of the panel's, a value it could not read, or an unknown field (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/settings/invitations \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"settings":null}'
// 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/settings/invitations", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"settings": null
}),
});
const data = await res.json();
{
"settings": {
"cancellation": "off",
"primary_email": "required",
"primary_name": "optional",
"refund_mode": "none"
}
}
Get invitation fields
GET /events/{slug}/settings/invitations/fields
GET /events/{slug}/settings/invitations/fields lists active person fields. Each field includes field_id, key, label, kind, its appearance for the primary invitee and guests, and ticket_types: the event's ticket types that ask it. A ticket type's question reads default until the event changes how it is asked.
Both routes require the settings.manage permission.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
fieldsarray of objectfields[].field_idintegerfields[].ticket_typesarray of object- The ticket types offered at this scope that ask this question, so an RSVP for one of them asks it unless this scope turns it off. Empty for a question only the invitation asks.
fields[].ticket_types[].idintegerfields[].ticket_types[].namestringfields[].ticket_types[].requiredbooleanfields[].keystringfields[].labelstringfields[].kindstring
Responses
200OK401No 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/settings/invitations/fields \
-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/settings/invitations/fields", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"fields": [
{
"field_id": 1,
"ticket_types": [
{
"id": 1,
"name": "Ada Lovelace",
"required": true
}
],
"key": "example",
"label": "example",
"kind": "ticket",
"primary": "optional",
"guest": "optional"
}
]
}
Update invitation fields
PATCH /events/{slug}/settings/invitations/fields
PATCH /events/{slug}/settings/invitations/fields updates the supplied field and audience cells. Each cell uses field_id, audience (primary or guest), and appearance: optional, required, readonly, secret, off, or default. default is only for a question a ticket type asks, and means it is asked the way the ticket asks it. optional is refused for a question a ticket type requires.
Unmentioned cells stay unchanged. The whole patch is rejected if any row is invalid. Duplicate cells are rejected. Event fields, archived fields, unknown fields, and default for a question no ticket type asks are refused.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Request body
Response fields
fieldsarray of objectfields[].field_idintegerfields[].ticket_typesarray of object- The ticket types offered at this scope that ask this question, so an RSVP for one of them asks it unless this scope turns it off. Empty for a question only the invitation asks.
fields[].ticket_types[].idintegerfields[].ticket_types[].namestringfields[].ticket_types[].requiredbooleanfields[].keystringfields[].labelstringfields[].kindstring
Responses
200OK401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Each cell needs field_id, audience (primary or guest), and appearance. Each field and audience pair must be unique.500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/settings/invitations/fields \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"fields":[{"field_id":1,"audience":"primary","appearance":"optional"}]}'
// 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/settings/invitations/fields", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"fields": [
{
"field_id": 1,
"audience": "primary",
"appearance": "optional"
}
]
}),
});
const data = await res.json();
{
"fields": [
{
"field_id": 1,
"ticket_types": [
{
"id": 1,
"name": "Ada Lovelace",
"required": true
}
],
"key": "example",
"label": "example",
"kind": "ticket",
"primary": "optional",
"guest": "optional"
}
]
}
Get the event's languages
GET /events/{slug}/settings/languages
The language the event's own words are written in, and the ones it offers beside it.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
Responses
200The panel.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/settings/languages \
-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/settings/languages", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"default": "en",
"extra": [
"ga"
]
}
Update the event's languages
PATCH /events/{slug}/settings/languages
extra replaces the offered set when sent. Promoting a language the event is already TRANSLATED into moves the words rather than relabelling them: every string written in it becomes the event's own text, and the text it had becomes a translation into the language it is leaving. That is one-way, so it answers 409 words_would_move with the count until the body says confirm: true. The outgoing language is then kept in extra on its own, or its text would be stranded the other way round.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Request body
confirmboolean- Accepts the one-way move a new default makes when the event is already translated into it.
Response fields
Responses
200The panel as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The default would move what is written in the new language (words_would_move, withmovessaying how many strings).422A language this product does not know, or an unknown field (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/settings/languages \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"default":"ga"}'
// 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/settings/languages", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"default": "ga"
}),
});
const data = await res.json();
{
"default": "en",
"extra": [
"ga"
]
}
Get the event's own document numbering
GET /events/{slug}/settings/numbering
Whether this event numbers its receipts, invoices, credit notes and quotes with prefixes of its own, counting from 1, rather than drawing from the account's series (GET /invoicing/numbering). The prefixes are kept while own is false, so switching back finds them again.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
ownboolean- Whether this event uses its own prefixes. False means it draws from the account's series.
account_numberedinteger- How many of this event's documents already carry a number from the account's series. Switching never changes them.
kindsarray of DocumentNumberingKind- One per kind.
prefixis blank for a kind nobody has typed one for, andnextis blank until there is a prefix. kinds[].prefixstring- The TEMPLATE, not the rendered prefix: it may carry
{YYYY},{YY},{MM}and{EVENT}, over a deliberately narrow alphabet (letters, digits and- _ / . #and a space). This string is printed on an accounting document and typed into other people's systems. kinds[].resetstringnever, orannualfor a counter that restarts each January. An annual reset needs the year in the prefix or the suffix.kinds[].suffixstring- What comes after the count, a template like
prefixwith the same placeholders and alphabet. It starts with a letter or a mark, never a digit or a placeholder, so it can't run into the count.""is no suffix. kinds[].nextstring- What the next number of this kind WOULD look like. Reading it allocates nothing. Counted against the account's newest event, which is what a per-event series means anything against.
Responses
200The event's numbering.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/settings/numbering \
-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/settings/numbering", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"own": true,
"account_numbered": 3,
"kinds": [
{
"kind": "invoice",
"prefix": "INV-{YYYY}-",
"pad": 5,
"reset": "annual",
"suffix": "-{YY}",
"next": "INV-2026-00043"
}
]
}
Change the event's own document numbering
PATCH /events/{slug}/settings/numbering
Only the keys present change, and inside kinds only the fields present change. Validated and stored ALL AT ONCE: a refusal on one kind stores nothing.
Switching changes what the event issues from now on. Documents already issued keep their numbers, and its own series starts at 1.
A prefix that could print a number twice is refused (invalid_body). That is the account's prefix for the same kind, another event's own prefix, or a prefix that would print a number already on a document. Turning own on also needs a prefix for every kind.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Request body
ownboolean
Response fields
ownboolean- Whether this event uses its own prefixes. False means it draws from the account's series.
account_numberedinteger- How many of this event's documents already carry a number from the account's series. Switching never changes them.
kindsarray of DocumentNumberingKind- One per kind.
prefixis blank for a kind nobody has typed one for, andnextis blank until there is a prefix. kinds[].prefixstring- The TEMPLATE, not the rendered prefix: it may carry
{YYYY},{YY},{MM}and{EVENT}, over a deliberately narrow alphabet (letters, digits and- _ / . #and a space). This string is printed on an accounting document and typed into other people's systems. kinds[].resetstringnever, orannualfor a counter that restarts each January. An annual reset needs the year in the prefix or the suffix.kinds[].suffixstring- What comes after the count, a template like
prefixwith the same placeholders and alphabet. It starts with a letter or a mark, never a digit or a placeholder, so it can't run into the count.""is no suffix. kinds[].nextstring- What the next number of this kind WOULD look like. Reading it allocates nothing. Counted against the account's newest event, which is what a per-event series means anything against.
Responses
200The event's numbering as it now stands.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body was unreadable or carried an unknown field, a value was invalid, a prefix was missing, or a prefix could print a number another series prints or that is already on a document (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/settings/numbering \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"own":true}'
// 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/settings/numbering", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"own": true
}),
});
const data = await res.json();
{
"own": true,
"account_numbered": 3,
"kinds": [
{
"kind": "invoice",
"prefix": "INV-{YYYY}-",
"pad": 5,
"reset": "annual",
"suffix": "-{YY}",
"next": "INV-2026-00043"
}
]
}
Get the payment settings
GET /events/{slug}/settings/payment
How the event takes money. The base currency is not here: it is fixed at creation and reported by the event itself.
Requires payment.manage
Path parameters
slugstring required- The event's slug.
Response fields
stripe_accountstring- Which of the account's Stripe accounts this event's new card payments go into: an account id, empty for the account's default, or
nonewhen nobody has picked one and the event takes no card payments. Orders already paid stay with the account they were paid into. checkout_modestring- Where buyers pay: hosted (Stripe's own page, the zero-JS baseline) or custom (on the event's own pages, with Stripe's payment fields). Events saved with the retired value embedded read back as custom.
- one of
hostedcustom hold_minutesinteger- How many minutes a buyer's selection is held for them while they fill in their details, 5 to 60. A hold is extended while nobody else is waiting for the tickets.
stripe_customers_enabledbooleanbank_transfer_enabledbooleanbank_transfer_reminder_daysstring- Days before the due date to remind, comma-separated. Normalized on write.
bank_transfer_overdue_daysstring- Days after the due date to chase, comma-separated. Normalized on write.
bank_transfer_auto_cancel_daysinteger- Days overdue before the order is released; 0 is off, 365 the ceiling.
paypal_enabledbooleanexpress_checkout_enabledboolean- Whether buyers are offered Apple Pay, Google Pay, Link and PayPal as one-tap shortcuts. Card payment stays either way, and PayPal stays where it is the only way to pay. Off here wins over an audience's own
show_express_checkout. extra_currenciesarray of string- Currencies this event sells in besides its own, in the account's order.
Responses
200The panel.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/settings/payment \
-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/settings/payment", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"payment_expiry_days": 0,
"stripe_account": "acct_1MEXAMPLE",
"checkout_mode": "custom",
"hold_minutes": 15,
"stripe_customers_enabled": false,
"bank_transfer_enabled": false,
"bank_transfer_due_days": 14,
"bank_transfer_reminder_days": "1,3,7",
"bank_transfer_overdue_days": "1,7",
"bank_transfer_auto_cancel_days": 0,
"paypal_enabled": false,
"express_checkout_enabled": true,
"extra_currencies": [
"usd",
"gbp"
],
"currency_assignment": ""
}
Update the payment settings
PATCH /events/{slug}/settings/payment
One statement, so a save can never land half of a payment method. The reminder schedules are normalized on write — parsed, deduped, put in order — so the sweeper never meets a value it has to guess about. extra_currencies replaces the whole list and comes back in the account's own order; a code the account cannot add, or the event's own currency, is refused.
Requires payment.manage
Path parameters
slugstring required- The event's slug.
Request body
stripe_accountstring- One of the account's Stripe accounts (see the Stripe connection's
stripe_accounts). Anything else is refused. Changing it moves only the event's new card payments. checkout_modestring- Where buyers pay: hosted (Stripe's own page, the zero-JS baseline) or custom (on the event's own pages, with Stripe's payment fields). embedded is a retired alias, still accepted and stored as custom.
- one of
hostedcustomembedded hold_minutesinteger- How many minutes a buyer's selection is held for them while they fill in their details, 5 to 60. A hold is extended while nobody else is waiting for the tickets.
stripe_customers_enabledbooleanbank_transfer_enabledbooleanbank_transfer_reminder_daysstring- Days before the due date to remind, comma-separated. Normalized on write.
bank_transfer_overdue_daysstring- Days after the due date to chase, comma-separated. Normalized on write.
bank_transfer_auto_cancel_daysinteger- Days overdue before the order is released; 0 is off, 365 the ceiling.
paypal_enabledbooleanexpress_checkout_enabledboolean- Whether buyers are offered Apple Pay, Google Pay, Link and PayPal as one-tap shortcuts. Card payment stays either way, and PayPal stays where it is the only way to pay. Off here wins over an audience's own
show_express_checkout. extra_currenciesarray of string- Currencies this event sells in besides its own, in the account's order.
Response fields
stripe_accountstring- Which of the account's Stripe accounts this event's new card payments go into: an account id, empty for the account's default, or
nonewhen nobody has picked one and the event takes no card payments. Orders already paid stay with the account they were paid into. checkout_modestring- Where buyers pay: hosted (Stripe's own page, the zero-JS baseline) or custom (on the event's own pages, with Stripe's payment fields). Events saved with the retired value embedded read back as custom.
- one of
hostedcustom hold_minutesinteger- How many minutes a buyer's selection is held for them while they fill in their details, 5 to 60. A hold is extended while nobody else is waiting for the tickets.
stripe_customers_enabledbooleanbank_transfer_enabledbooleanbank_transfer_reminder_daysstring- Days before the due date to remind, comma-separated. Normalized on write.
bank_transfer_overdue_daysstring- Days after the due date to chase, comma-separated. Normalized on write.
bank_transfer_auto_cancel_daysinteger- Days overdue before the order is released; 0 is off, 365 the ceiling.
paypal_enabledbooleanexpress_checkout_enabledboolean- Whether buyers are offered Apple Pay, Google Pay, Link and PayPal as one-tap shortcuts. Card payment stays either way, and PayPal stays where it is the only way to pay. Off here wins over an audience's own
show_express_checkout. extra_currenciesarray of string- Currencies this event sells in besides its own, in the account's order.
Responses
200The panel as it now stands.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 held a value this panel does not know (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/settings/payment \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"payment_expiry_days":0}'
// 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/settings/payment", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"payment_expiry_days": 0
}),
});
const data = await res.json();
{
"payment_expiry_days": 0,
"stripe_account": "acct_1MEXAMPLE",
"checkout_mode": "custom",
"hold_minutes": 15,
"stripe_customers_enabled": false,
"bank_transfer_enabled": false,
"bank_transfer_due_days": 14,
"bank_transfer_reminder_days": "1,3,7",
"bank_transfer_overdue_days": "1,7",
"bank_transfer_auto_cancel_days": 0,
"paypal_enabled": false,
"express_checkout_enabled": true,
"extra_currencies": [
"usd",
"gbp"
],
"currency_assignment": ""
}
Get the sales settings
GET /events/{slug}/settings/registration
When selling stops, when the questions get asked, and quotes.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Response fields
sales_end_modestring- "ends" (when the event ends), "never", or "custom" with sales_end_at saying when.
- one of
endsnevercustom field_collection_timingstring- Whether the questions are asked before paying or after.
- one of
before_paymentafter_payment require_holder_namebooleanrequire_holder_emailbooleanrequire_holder_answersboolean- Whether a ticket the booker gives to somebody else must have its required questions answered before it is sent.
qr_gatestring- When a ticket's QR code appears: always, once it is named, or once its questions are done.
- one of
alwaysnameddone min_tickets_per_orderinteger- The fewest tickets one buyer's order may hold, 0 for no limit. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it.
max_tickets_per_orderinteger- The most tickets one buyer's order may hold, 0 for no limit. Counted as min_tickets_per_order is, and an addition to an order counts what the order already holds.
capacityinteger or null- The most tickets the whole event can sell, across every ticket type, null for no limit. Once the event reaches it every ticket type is off sale, whatever each has left. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it. An organizer's own orders in the admin may go past it.
Responses
200The panel.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/settings/registration \
-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/settings/registration", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"sales_end_mode": "ends",
"sales_end_at": null,
"field_collection_timing": "before_payment",
"require_holder_name": false,
"require_holder_email": false,
"require_holder_answers": false,
"qr_gate": "always",
"quotes_enabled": false,
"quotes_on_main": false,
"min_tickets_per_order": 0,
"max_tickets_per_order": 6,
"capacity": 500,
"quote_audience_ids": [
6
]
}
Update the sales settings
PATCH /events/{slug}/settings/registration
Absent fields are unchanged, an explicit null and an unknown field are refused. sales_end_mode decides and sales_end_at is only read in custom, so a stored date and a "never close" flag can never both claim to be the answer; custom with no date is refused. quote_audience_ids replaces which audience pages offer a quote button.
Requires settings.manage
Path parameters
slugstring required- The event's slug.
Request body
sales_end_modestring- "ends" (when the event ends), "never", or "custom" with sales_end_at saying when.
- one of
endsnevercustom field_collection_timingstring- Whether the questions are asked before paying or after.
- one of
before_paymentafter_payment require_holder_namebooleanrequire_holder_emailbooleanrequire_holder_answersboolean- Whether a ticket the booker gives to somebody else must have its required questions answered before it is sent.
qr_gatestring- When a ticket's QR code appears: always, once it is named, or once its questions are done.
- one of
alwaysnameddone min_tickets_per_orderinteger- The fewest tickets one buyer's order may hold, 0 for no limit. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it.
max_tickets_per_orderinteger- The most tickets one buyer's order may hold, 0 for no limit. Counted as min_tickets_per_order is, and an addition to an order counts what the order already holds.
capacityinteger or null- The most tickets the whole event can sell, across every ticket type, null for no limit. Once the event reaches it every ticket type is off sale, whatever each has left. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it. An organizer's own orders in the admin may go past it.
Response fields
sales_end_modestring- "ends" (when the event ends), "never", or "custom" with sales_end_at saying when.
- one of
endsnevercustom field_collection_timingstring- Whether the questions are asked before paying or after.
- one of
before_paymentafter_payment require_holder_namebooleanrequire_holder_emailbooleanrequire_holder_answersboolean- Whether a ticket the booker gives to somebody else must have its required questions answered before it is sent.
qr_gatestring- When a ticket's QR code appears: always, once it is named, or once its questions are done.
- one of
alwaysnameddone min_tickets_per_orderinteger- The fewest tickets one buyer's order may hold, 0 for no limit. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it.
max_tickets_per_orderinteger- The most tickets one buyer's order may hold, 0 for no limit. Counted as min_tickets_per_order is, and an addition to an order counts what the order already holds.
capacityinteger or null- The most tickets the whole event can sell, across every ticket type, null for no limit. Once the event reaches it every ticket type is off sale, whatever each has left. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it. An organizer's own orders in the admin may go past it.
Responses
200The panel as it now stands.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 held a value this panel does not know (invalid_body).500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/settings/registration \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"sales_end_mode":"ends"}'
// 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/settings/registration", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"sales_end_mode": "ends"
}),
});
const data = await res.json();
{
"sales_end_mode": "ends",
"sales_end_at": null,
"field_collection_timing": "before_payment",
"require_holder_name": false,
"require_holder_email": false,
"require_holder_answers": false,
"qr_gate": "always",
"quotes_enabled": false,
"quotes_on_main": false,
"min_tickets_per_order": 0,
"max_tickets_per_order": 6,
"capacity": 500,
"quote_audience_ids": [
6
]
}
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[].completed_atstring (RFC 3339) or null- When the order was finished: paid, placed by bank transfer, or made by the organizer. The moment
order.completedfired. Null until then, and null for an order finished before this field existed. 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[].items[].unit_price_centsinteger- Integer cents; see the order's currency. The ticket type's price, also on a seat that was given away free by a group discount or a free-with rule: what was charged is the matching
linesentry'stotal_cents, and itscharged_quantitysays how many were paid for. 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[].charged_quantityinteger- How many of
quantitywere paid for, never more thanquantity. Fewer when a group discount or a free-with rule gave some away, and 0 on a ticket that came free with another.total_centsisunit_price_centstimes this, plus any tax added on top;subtotal_centsis the price of allquantitybefore the free ones came off. orders[].lines[].subtotal_centsinteger- Integer cents, before any tax added on top:
unit_price_centstimesquantity, before any seats given away free (seecharged_quantity). orders[].lines[].tax_centsinteger- Integer cents. The tax snapshot taken when the line was written; 0 when the line carries no tax.
orders[].lines[].taxesarray of object- Each tax rate the line charges, with its own share of
tax_cents, which stays the line's whole tax: one entry for a line on one rate, two for a line on two (GST and QST). Absent on a line with no tax. orders[].lines[].taxes[].rate_milli_pctinteger- The rate in thousandths of a percent, so 9.975% is 9975.
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[].lines[].eventstring- The slug of the event this line's ticket type belongs to, present only when that is not the order's own event. A ticket moved to another event keeps its order, and all of its money, at the event it was paid at, so the line recording the move is on that order for a type at the other event. Absent on every other line.
orders[].lines[].place_releasedboolean- True on an active line whose ticket has moved onto another ticket type or event: its money still counts, and the place it held is now held by the line carrying that ticket's ticket_ref. Absent otherwise.
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[].discounts[].on_additionboolean- True for a code typed when tickets were added to the order after it was placed. It came off that addition's own payment. Listed once the addition is paid, which is when its lines count towards
amount_cents. 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.
orders[].companyobject- Who the order was bought FOR, when the buyer ticked "I'm buying for a company or organization" at checkout. Null on an order somebody placed for themselves, which is most of them — this is the party the invoice is made out to, where
nameandemailare the person who paid. Reconcile a finance system against this, not againstname. orders[].company.namestring- The legal entity the invoice is made out to. Always present when
companyis. orders[].company.vat_numberstring- As typed by the buyer, unnormalized and unverified. Empty when they did not give one — it is never required to complete a purchase.
orders[].company.address_line1string- The billing address, as one line. Empty when the buyer did not give one.
orders[].company.subdivisionstring- State, province or prefecture. Only collected for countries whose addresses carry one, so it is empty for most of Europe even when the rest of the address is filled in. Where Tito has that country's list (the US, Canada, Australia, Brazil, China, India, Indonesia and Mexico) the buyer picks from it and this is the ISO 3166-2 code, so "OH" rather than "Ohio"; elsewhere it is what they typed.
orders[].company.po_referencestring- The purchase order number the BUYER issued (BT-13), printed on the invoice. Tito's own order number is BT-14 and is
ref. orders[].company.einvoice_idstring- The country-specific routing identifier an e-invoice needs, where the buyer's country has one and the organizer turned
sends_einvoiceson. Read it againstcountry: Italy's is a Codice Destinatario (seven characters) or a PEC address, France's is the buyer's nine-digit SIREN, Germany's is a Leitweg-ID and only a public authority has one. Empty for every other country, because their invoices route onvat_number. Never format-checked at checkout: a wrong value is an invoice that gets re-sent, and refusing the sale would be worse. orders[].company.vat_statusstring- What the EU VAT register said about
vat_number. Empty until there is an answer and empty forever for a country the register does not cover. "unavailable" means we could not GET an answer, not that the number is bad: a member state's registry is down several times a month, and reading it as a verdict would act on an outage. - one of
validinvalidunavailable orders[].company.vat_registered_namestring- The register's own name for the holder of the number, where the member state returns one. It is evidence that the number belongs to
name; nothing compares the two automatically. Empty when the register gave no name, which some member states never do even for a valid number. orders[].buyerobject- What the PERSON said about themselves, as opposed to the company they may have been buying for. Null unless the organizer turned
sends_einvoiceson, which is what puts the country question on the checkout at all. orders[].buyer.countrystring- The buyer's own country as ISO 3166-1 alpha-2, upper case. Present on a company order too: with the switch on this is the only country control on the checkout, so it is the country of the purchase either way.
orders[].buyer.einvoice_idstring- The buyer's own tax number, where their country wants one on an invoice made out to a person: Italy's codice fiscale, Portugal's NIF, Spain's NIF. Empty for every other country, and always empty when
companyis set, because an invoice to a company carries the company's identifiers rather than the buyer's. PERSONAL DATA in a way a company VAT number is not — a codice fiscale encodes date of birth, sex and birthplace — so handle it like a name or an address, not like a reference code. orders[].passed_parametersobject- The allowlisted query params the buyer's link carried, kept on the order. An object rather than a list of pairs because a key appears at most once per order.
{}when the order carries none, never null. Which params are kept is the event'spassed_params_onandpassed_params_pattern. orders[].stripe_accountstring- The Stripe account (acct_…) the order's money was paid into. Its refunds go back through that account, whichever account its event pays into now. Empty for an order that never met Stripe.
orders[].cardobject- The card that paid the order, as the buyer knows it: the brand and the last four digits, read off the Stripe charge shortly after payment and printed on the receipt as "Visa ending 4242". Null where no card is recorded: an order paid by PayPal or bank transfer, a comp, a free order, or an order paid before Tito started keeping this. Never the full number, the expiry, or anything that could be charged.
orders[].card.brandstring- Stripe's own brand code, lower case:
visa,mastercard,amex,discover,diners,jcb,unionpay, and others as Stripe adds them. orders[].anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
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",
"completed_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,
"charged_quantity": 2,
"unit_price_cents": 14900,
"subtotal_cents": 29800,
"tax_cents": 0,
"taxes": [
{
"name": "GST",
"rate_milli_pct": 5000,
"tax_cents": 250
}
],
"total_cents": 29800,
"currency": "eur",
"status": "active",
"tier_id": 0,
"band_id": 0,
"ticket_ref": "",
"event": "",
"place_released": false
}
],
"discounts": [
{
"code": "EARLYBIRD",
"amount_cents": 2000,
"currency": "eur",
"on_addition": false
}
],
"price_adjustments": [
{
"label": "Speaker discount",
"amount_cents": -5000,
"currency": "eur"
}
],
"company": {
"name": "Analytical Engines Srl",
"country": "IT",
"vat_number": "IT02659550135",
"address_line1": "Via Ada Byron 12",
"city": "Torino",
"postcode": "10121",
"subdivision": "OH",
"po_reference": "PO-4471",
"einvoice_id": "ABC1234",
"vat_status": "valid",
"vat_registered_name": "ANALYTICAL ENGINES LTD"
},
"buyer": {
"country": "IT",
"einvoice_id": "RSSMRA85T10A562S"
},
"passed_parameters": {
"utm_campaign": "spring",
"utm_source": "newsletter"
},
"stripe_account": "acct_1MEXAMPLE",
"card": {
"brand": "visa",
"last4": "4242"
},
"anonymized_at": null
}
],
"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 is trimmed to what is left, down to none, and the tickets are still sold, as they would be on the event page.
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.
passed_parametersobject- Campaign attribution to record against this order. Ignored when the event has
passed_params_onfalse, because off means no rows whatever the source. The event'spassed_params_patterndoes NOT apply: that pattern decides which of a buyer's URL params to keep, and this caller has no URL. The limits do apply, at most 10 pairs, keys of up to 40 characters matching[a-z0-9_-], values of up to 200, and 1024 bytes in total. Anything over a limit is dropped rather than truncated, and anything malformed is dropped without an error.
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, at the account's own address (the event's own domain when it has one). Present only while the order is pending. A capability URL: treat it as a secret.
urlstring- Absolute URL of the buyer's order page, at the account's own address (the event's own domain when it has one). 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, at the account's own address (the event's own domain when it has one). Present only while the order is pending. A capability URL: treat it as a secret.
urlstring- Absolute URL of the buyer's order page, at the account's own address (the event's own domain when it has one). 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. Two checks produce it: the availability read the shop's own event page uses, which names the type 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/…"
}
List quotes
GET /events/{slug}/quotes
A quote is a priced selection sent to a buyer, who pays for it later from their own page; nothing is held until it converts. Quotes are addressed by their ref; the buyer's page is the url, a bearer link, reported because sending it is what a quote is for. Every write is recorded in the event's activity against the key. Newest first; a quote past its expiry reads as expired.
Requires orders.view
Path parameters
slugstring required- The event's slug.
Response fields
quotesarray of Quotequotes[].idintegerquotes[].emailstringquotes[].namestringquotes[].orgstringquotes[].statusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
- one of
opensubmittedrejectedimportedexpiredcanceled quotes[].currencystringquotes[].promo_discount_centsintegerquotes[].promo_codesarray of object- Every code the quote was priced with, in the order the buyer added them, with what each takes off. promo_code is the first and promo_discount_cents the total.
quotes[].promo_codes[].codestringquotes[].promo_codes[].discount_centsintegerquotes[].linesarray of QuoteLinequotes[].lines[].ticket_type_idinteger or nullquotes[].lines[].product_idinteger or nullquotes[].lines[].namestringquotes[].lines[].quantityintegerquotes[].lines[].charge_quantityinteger- How many of quantity are paid for; a group discount zeroes the free ones.
quotes[].lines[].unit_price_centsintegerquotes[].lines[].tax_centsintegerquotes[].lines[].tax_namestringquotes[].lines[].taxesarray of object- Each tax rate the line is quoted at, with its own share of
tax_cents: one entry for one rate, two for two. Absent on a line with no tax. quotes[].lines[].taxes[].rate_milli_pctinteger- The rate in thousandths of a percent, so 9.975% is 9975.
quotes[].expires_atstring (RFC 3339) or nullquotes[].created_atstring (RFC 3339) or nullquotes[].needs_approvalboolean- An open quote whose buyer asks for approval. The buyer's page sends it to an approver instead of converting it.
quotes[].approval.approver.refstringquotes[].approval.approver.namestringquotes[].approval.submitted_atstring (RFC 3339) or nullquotes[].approval.rejected_bystring or null- The approver who rejected it, null unless it was rejected.
quotes[].approval.rejected_atstring (RFC 3339) or nullquotes[].approval.po_numberstring or null- The purchase order that approved the quote, null until one is recorded. Recording it converts the quote.
quotes[].approval.recorded_bystring or null- The approver who recorded the purchase order, null when the organizer recorded it or none is recorded.
quotes[].approval.recorded_doorstring or null- Who recorded it:
approver(an approver on the buyer it was sent to),buyer(an approver on the quote's own buyer) ororganizer. Null until recorded. - one of
approverbuyerorganizer<nil> quotes[].approval.recorded_atstring (RFC 3339) or nullquotes[].buyerobject or null- The buyer the quote is filed under, null for none: a quote made by somebody signed in, or composed for an address that is one of exactly one buyer's people. That buyer's approval applies to it.
quotes[].buyer.refstringquotes[].buyer.namestringquotes[].warningobject- Only on a create's answer, and only when the quote could not be filed under a buyer it might belong to:
several_buyerswhen the email is one of the people on more than one buyer. quotes[].warning.codestringquotes[].warning.messagestring
Responses
200Quotes.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/quotes \
-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/quotes", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"quotes": [
{
"id": 12,
"ref": "2QY8FN",
"url": "https://acct.go.tito.io/annual/quotes/1qR20rFnPEV70bqeGNLJZg",
"email": "accounts@example.com",
"name": "Mary Byrne",
"org": "St Mary's",
"status": "open",
"amount_cents": 20000,
"currency": "eur",
"promo_code": "",
"promo_discount_cents": 0,
"promo_codes": [
{
"code": "SCHOOL10",
"discount_cents": 500
}
],
"locale": "en",
"lines": [
{
"charge_quantity": 2,
"kind": "ticket",
"name": "VIP",
"product_id": null,
"quantity": 2,
"tax_cents": 0,
"tax_name": "",
"ticket_type_id": 8,
"unit_price_cents": 10000
}
],
"order_ref": null,
"expires_at": "2026-09-21T22:50:00Z",
"created_at": "2026-09-07T22:50:00Z",
"needs_approval": false,
"approval": {
"approver": {
"name": "Example Ltd",
"ref": "k3m8p1"
},
"po_number": null,
"recorded_at": null,
"recorded_by": null,
"recorded_door": null,
"rejected_at": null,
"rejected_by": null,
"rejected_reason": null,
"submitted_at": "2026-09-08T09:15:00Z",
"submitted_by": "alex@example.com"
},
"buyer": {
"name": "Example Ltd Design",
"ref": "p2q7r4"
},
"warning": {
"code": "several_buyers",
"message": "example"
}
}
]
}
Create a quote
POST /events/{slug}/quotes
The composer: each line priced as the event page prices it in the quote's currency, or at the unit_price_cents given; group discounts, then the volume band the quantity reaches (a sent unit_price_cents wins over the band), then tax, as the shop applies them. Emailed to the buyer unless send: false. A line on a timed ticket type needs its slot chosen, which only the admin's composer can do today. When the email is one of exactly one buyer's people, the quote is filed under that buyer and its approval applies; on several, it is filed under none and the answer carries a warning.
Requires orders.manage
Path parameters
slugstring required- The event's slug.
Request body
namestringorgstringitemsarray of QuoteItemInput requireditems[].ticket_type_idinteger- One of the event's, priced in the quote's currency. Not with product_id.
Response fields
idintegeremailstringnamestringorgstringstatusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
- one of
opensubmittedrejectedimportedexpiredcanceled currencystringpromo_discount_centsintegerpromo_codesarray of object- Every code the quote was priced with, in the order the buyer added them, with what each takes off. promo_code is the first and promo_discount_cents the total.
promo_codes[].codestringpromo_codes[].discount_centsintegerlinesarray of QuoteLinelines[].ticket_type_idinteger or nulllines[].product_idinteger or nulllines[].namestringlines[].quantityintegerlines[].charge_quantityinteger- How many of quantity are paid for; a group discount zeroes the free ones.
lines[].unit_price_centsintegerlines[].tax_centsintegerlines[].tax_namestringlines[].taxesarray of object- Each tax rate the line is quoted at, with its own share of
tax_cents: one entry for one rate, two for two. Absent on a line with no tax. expires_atstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nullneeds_approvalboolean- An open quote whose buyer asks for approval. The buyer's page sends it to an approver instead of converting it.
approval.approver.refstringapproval.approver.namestringapproval.submitted_atstring (RFC 3339) or nullapproval.rejected_atstring (RFC 3339) or nullapproval.po_numberstring or null- The purchase order that approved the quote, null until one is recorded. Recording it converts the quote.
approval.recorded_bystring or null- The approver who recorded the purchase order, null when the organizer recorded it or none is recorded.
approval.recorded_doorstring or null- Who recorded it:
approver(an approver on the buyer it was sent to),buyer(an approver on the quote's own buyer) ororganizer. Null until recorded. - one of
approverbuyerorganizer<nil> approval.recorded_atstring (RFC 3339) or nullbuyerobject or null- The buyer the quote is filed under, null for none: a quote made by somebody signed in, or composed for an address that is one of exactly one buyer's people. That buyer's approval applies to it.
buyer.refstringbuyer.namestringwarningobject- Only on a create's answer, and only when the quote could not be filed under a buyer it might belong to:
several_buyerswhen the email is one of the people on more than one buyer. warning.codestringwarning.messagestring
Responses
201The new quote.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422The body was unreadable or carried an unknown field, the email is not one, no items, an item naming neither or both of a ticket type and a product, a quantity outside 1–500, a negative price, a type or product not priced in the currency on this event, one listed twice, or a currency the event does not sell in (invalid_body); a timed ticket type (needs_session); an extra sold only with a ticket and none of its tickets on the quote (needs_ticket).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/quotes \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"email":"accounts@example.com","items":[{"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/quotes", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"email": "accounts@example.com",
"items": [
{
"quantity": 2
}
]
}),
});
const data = await res.json();
{
"id": 12,
"ref": "2QY8FN",
"url": "https://acct.go.tito.io/annual/quotes/1qR20rFnPEV70bqeGNLJZg",
"email": "accounts@example.com",
"name": "Mary Byrne",
"org": "St Mary's",
"status": "open",
"amount_cents": 20000,
"currency": "eur",
"promo_code": "",
"promo_discount_cents": 0,
"promo_codes": [
{
"code": "SCHOOL10",
"discount_cents": 500
}
],
"locale": "en",
"lines": [
{
"charge_quantity": 2,
"kind": "ticket",
"name": "VIP",
"product_id": null,
"quantity": 2,
"tax_cents": 0,
"tax_name": "",
"ticket_type_id": 8,
"unit_price_cents": 10000
}
],
"order_ref": null,
"expires_at": "2026-09-21T22:50:00Z",
"created_at": "2026-09-07T22:50:00Z",
"needs_approval": false,
"approval": {
"approver": {
"name": "Example Ltd",
"ref": "k3m8p1"
},
"po_number": null,
"recorded_at": null,
"recorded_by": null,
"recorded_door": null,
"rejected_at": null,
"rejected_by": null,
"rejected_reason": null,
"submitted_at": "2026-09-08T09:15:00Z",
"submitted_by": "alex@example.com"
},
"buyer": {
"name": "Example Ltd Design",
"ref": "p2q7r4"
},
"warning": {
"code": "several_buyers",
"message": "example"
}
}
Get a quote
GET /events/{slug}/quotes/{ref}
One quote with its lines.
Requires orders.view
Path parameters
slugstring required- The event's slug.
refstring required- The quote's ref, as
GET /events/{slug}/quoteslists it.
Response fields
idintegeremailstringnamestringorgstringstatusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
- one of
opensubmittedrejectedimportedexpiredcanceled currencystringpromo_discount_centsintegerpromo_codesarray of object- Every code the quote was priced with, in the order the buyer added them, with what each takes off. promo_code is the first and promo_discount_cents the total.
promo_codes[].codestringpromo_codes[].discount_centsintegerlinesarray of QuoteLinelines[].ticket_type_idinteger or nulllines[].product_idinteger or nulllines[].namestringlines[].quantityintegerlines[].charge_quantityinteger- How many of quantity are paid for; a group discount zeroes the free ones.
lines[].unit_price_centsintegerlines[].tax_centsintegerlines[].tax_namestringlines[].taxesarray of object- Each tax rate the line is quoted at, with its own share of
tax_cents: one entry for one rate, two for two. Absent on a line with no tax. expires_atstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nullneeds_approvalboolean- An open quote whose buyer asks for approval. The buyer's page sends it to an approver instead of converting it.
approval.approver.refstringapproval.approver.namestringapproval.submitted_atstring (RFC 3339) or nullapproval.rejected_atstring (RFC 3339) or nullapproval.po_numberstring or null- The purchase order that approved the quote, null until one is recorded. Recording it converts the quote.
approval.recorded_bystring or null- The approver who recorded the purchase order, null when the organizer recorded it or none is recorded.
approval.recorded_doorstring or null- Who recorded it:
approver(an approver on the buyer it was sent to),buyer(an approver on the quote's own buyer) ororganizer. Null until recorded. - one of
approverbuyerorganizer<nil> approval.recorded_atstring (RFC 3339) or nullbuyerobject or null- The buyer the quote is filed under, null for none: a quote made by somebody signed in, or composed for an address that is one of exactly one buyer's people. That buyer's approval applies to it.
buyer.refstringbuyer.namestringwarningobject- Only on a create's answer, and only when the quote could not be filed under a buyer it might belong to:
several_buyerswhen the email is one of the people on more than one buyer. warning.codestringwarning.messagestring
Responses
200The quote.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/quotes/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/quotes/H7K2PX", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 12,
"ref": "2QY8FN",
"url": "https://acct.go.tito.io/annual/quotes/1qR20rFnPEV70bqeGNLJZg",
"email": "accounts@example.com",
"name": "Mary Byrne",
"org": "St Mary's",
"status": "open",
"amount_cents": 20000,
"currency": "eur",
"promo_code": "",
"promo_discount_cents": 0,
"promo_codes": [
{
"code": "SCHOOL10",
"discount_cents": 500
}
],
"locale": "en",
"lines": [
{
"charge_quantity": 2,
"kind": "ticket",
"name": "VIP",
"product_id": null,
"quantity": 2,
"tax_cents": 0,
"tax_name": "",
"ticket_type_id": 8,
"unit_price_cents": 10000
}
],
"order_ref": null,
"expires_at": "2026-09-21T22:50:00Z",
"created_at": "2026-09-07T22:50:00Z",
"needs_approval": false,
"approval": {
"approver": {
"name": "Example Ltd",
"ref": "k3m8p1"
},
"po_number": null,
"recorded_at": null,
"recorded_by": null,
"recorded_door": null,
"rejected_at": null,
"rejected_by": null,
"rejected_reason": null,
"submitted_at": "2026-09-08T09:15:00Z",
"submitted_by": "alex@example.com"
},
"buyer": {
"name": "Example Ltd Design",
"ref": "p2q7r4"
},
"warning": {
"code": "several_buyers",
"message": "example"
}
}
Cancel a quote
POST /events/{slug}/quotes/{ref}/cancel
The buyer's page stops offering it. An open quote or one sent for approval can be canceled.
Requires orders.manage
Path parameters
slugstring required- The event's slug.
refstring required- The quote's ref, as
GET /events/{slug}/quoteslists it.
Response fields
idintegeremailstringnamestringorgstringstatusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
- one of
opensubmittedrejectedimportedexpiredcanceled currencystringpromo_discount_centsintegerpromo_codesarray of object- Every code the quote was priced with, in the order the buyer added them, with what each takes off. promo_code is the first and promo_discount_cents the total.
promo_codes[].codestringpromo_codes[].discount_centsintegerlinesarray of QuoteLinelines[].ticket_type_idinteger or nulllines[].product_idinteger or nulllines[].namestringlines[].quantityintegerlines[].charge_quantityinteger- How many of quantity are paid for; a group discount zeroes the free ones.
lines[].unit_price_centsintegerlines[].tax_centsintegerlines[].tax_namestringlines[].taxesarray of object- Each tax rate the line is quoted at, with its own share of
tax_cents: one entry for one rate, two for two. Absent on a line with no tax. expires_atstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nullneeds_approvalboolean- An open quote whose buyer asks for approval. The buyer's page sends it to an approver instead of converting it.
approval.approver.refstringapproval.approver.namestringapproval.submitted_atstring (RFC 3339) or nullapproval.rejected_atstring (RFC 3339) or nullapproval.po_numberstring or null- The purchase order that approved the quote, null until one is recorded. Recording it converts the quote.
approval.recorded_bystring or null- The approver who recorded the purchase order, null when the organizer recorded it or none is recorded.
approval.recorded_doorstring or null- Who recorded it:
approver(an approver on the buyer it was sent to),buyer(an approver on the quote's own buyer) ororganizer. Null until recorded. - one of
approverbuyerorganizer<nil> approval.recorded_atstring (RFC 3339) or nullbuyerobject or null- The buyer the quote is filed under, null for none: a quote made by somebody signed in, or composed for an address that is one of exactly one buyer's people. That buyer's approval applies to it.
buyer.refstringbuyer.namestringwarningobject- Only on a create's answer, and only when the quote could not be filed under a buyer it might belong to:
several_buyerswhen the email is one of the people on more than one buyer. warning.codestringwarning.messagestring
Responses
200Canceled.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The quote is neither open nor submitted (not_open).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/quotes/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/events/annual-conf/quotes/H7K2PX/cancel", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 12,
"ref": "2QY8FN",
"url": "https://acct.go.tito.io/annual/quotes/1qR20rFnPEV70bqeGNLJZg",
"email": "accounts@example.com",
"name": "Mary Byrne",
"org": "St Mary's",
"status": "open",
"amount_cents": 20000,
"currency": "eur",
"promo_code": "",
"promo_discount_cents": 0,
"promo_codes": [
{
"code": "SCHOOL10",
"discount_cents": 500
}
],
"locale": "en",
"lines": [
{
"charge_quantity": 2,
"kind": "ticket",
"name": "VIP",
"product_id": null,
"quantity": 2,
"tax_cents": 0,
"tax_name": "",
"ticket_type_id": 8,
"unit_price_cents": 10000
}
],
"order_ref": null,
"expires_at": "2026-09-21T22:50:00Z",
"created_at": "2026-09-07T22:50:00Z",
"needs_approval": false,
"approval": {
"approver": {
"name": "Example Ltd",
"ref": "k3m8p1"
},
"po_number": null,
"recorded_at": null,
"recorded_by": null,
"recorded_door": null,
"rejected_at": null,
"rejected_by": null,
"rejected_reason": null,
"submitted_at": "2026-09-08T09:15:00Z",
"submitted_by": "alex@example.com"
},
"buyer": {
"name": "Example Ltd Design",
"ref": "p2q7r4"
},
"warning": {
"code": "several_buyers",
"message": "example"
}
}
Convert a quote to an order
POST /events/{slug}/quotes/{ref}/convert
Makes the pending order — the one point stock binds. A quoted price is honoured; a discount the quote named that has since changed or gone moves the total, and that is refused until confirm: true with confirm_total_cents set to the now_cents of the 409 repriced body, which carries was_cents and now_cents. A total that moves again is refused again. Answers the quote with its order_ref. A quote sent for approval (submitted) converts here too, once its approver has agreed. With po_number, it records the approver's purchase order against a submitted quote: the claim that converts the quote writes the number, so of two racing records only one converts, and the invoice is issued to the quote's own buyer, under that buyer's payment terms, with the number as its PO reference. The quote's approval then reads the number, with recorded_door organizer.
Requires orders.manage
Path parameters
slugstring required- The event's slug.
refstring required- The quote's ref, as
GET /events/{slug}/quoteslists it.
Request body
confirmboolean- Convert at the new total when the quote's total moved, with
confirm_total_centsset to that total. po_numberstring- A purchase order number the quote's approver sent you. Only on a submitted quote: recording it approves and converts the quote, and puts the number on the invoice issued to the quote's buyer.
confirm_total_centsinteger- The new total
confirmis for: thenow_centsof the409 repricedanswer.confirmwithout it confirms nothing. A confirmation counts only while the total is still that figure, so a price that moves again is refused again.
Response fields
idintegeremailstringnamestringorgstringstatusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
- one of
opensubmittedrejectedimportedexpiredcanceled currencystringpromo_discount_centsintegerpromo_codesarray of object- Every code the quote was priced with, in the order the buyer added them, with what each takes off. promo_code is the first and promo_discount_cents the total.
promo_codes[].codestringpromo_codes[].discount_centsintegerlinesarray of QuoteLinelines[].ticket_type_idinteger or nulllines[].product_idinteger or nulllines[].namestringlines[].quantityintegerlines[].charge_quantityinteger- How many of quantity are paid for; a group discount zeroes the free ones.
lines[].unit_price_centsintegerlines[].tax_centsintegerlines[].tax_namestringlines[].taxesarray of object- Each tax rate the line is quoted at, with its own share of
tax_cents: one entry for one rate, two for two. Absent on a line with no tax. expires_atstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nullneeds_approvalboolean- An open quote whose buyer asks for approval. The buyer's page sends it to an approver instead of converting it.
approval.approver.refstringapproval.approver.namestringapproval.submitted_atstring (RFC 3339) or nullapproval.rejected_atstring (RFC 3339) or nullapproval.po_numberstring or null- The purchase order that approved the quote, null until one is recorded. Recording it converts the quote.
approval.recorded_bystring or null- The approver who recorded the purchase order, null when the organizer recorded it or none is recorded.
approval.recorded_doorstring or null- Who recorded it:
approver(an approver on the buyer it was sent to),buyer(an approver on the quote's own buyer) ororganizer. Null until recorded. - one of
approverbuyerorganizer<nil> approval.recorded_atstring (RFC 3339) or nullbuyerobject or null- The buyer the quote is filed under, null for none: a quote made by somebody signed in, or composed for an address that is one of exactly one buyer's people. That buyer's approval applies to it.
buyer.refstringbuyer.namestringwarningobject- Only on a create's answer, and only when the quote could not be filed under a buyer it might belong to:
several_buyerswhen the email is one of the people on more than one buyer. warning.codestringwarning.messagestring
Responses
200Converted.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409A line is no longer available (unavailable); the total moved and was not confirmed (repriced, withwas_centsandnow_cents); another convert won the claim (already_converted); the quote is neither open nor submitted (not_open);po_numberon a quote that isn't waiting for approval (not_submitted).422The body was unreadable, orpo_numberis longer than 200 characters (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/quotes/H7K2PX/convert \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"confirm":true}'
// 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/quotes/H7K2PX/convert", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"confirm": true
}),
});
const data = await res.json();
{
"id": 12,
"ref": "2QY8FN",
"url": "https://acct.go.tito.io/annual/quotes/1qR20rFnPEV70bqeGNLJZg",
"email": "accounts@example.com",
"name": "Mary Byrne",
"org": "St Mary's",
"status": "open",
"amount_cents": 20000,
"currency": "eur",
"promo_code": "",
"promo_discount_cents": 0,
"promo_codes": [
{
"code": "SCHOOL10",
"discount_cents": 500
}
],
"locale": "en",
"lines": [
{
"charge_quantity": 2,
"kind": "ticket",
"name": "VIP",
"product_id": null,
"quantity": 2,
"tax_cents": 0,
"tax_name": "",
"ticket_type_id": 8,
"unit_price_cents": 10000
}
],
"order_ref": null,
"expires_at": "2026-09-21T22:50:00Z",
"created_at": "2026-09-07T22:50:00Z",
"needs_approval": false,
"approval": {
"approver": {
"name": "Example Ltd",
"ref": "k3m8p1"
},
"po_number": null,
"recorded_at": null,
"recorded_by": null,
"recorded_door": null,
"rejected_at": null,
"rejected_by": null,
"rejected_reason": null,
"submitted_at": "2026-09-08T09:15:00Z",
"submitted_by": "alex@example.com"
},
"buyer": {
"name": "Example Ltd Design",
"ref": "p2q7r4"
},
"warning": {
"code": "several_buyers",
"message": "example"
}
}
Email a quote again
POST /events/{slug}/quotes/{ref}/send
Sends the buyer their quote in the language it was made in.
Requires orders.manage
Path parameters
slugstring required- The event's slug.
refstring required- The quote's ref, as
GET /events/{slug}/quoteslists it.
Response fields
idintegeremailstringnamestringorgstringstatusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
- one of
opensubmittedrejectedimportedexpiredcanceled currencystringpromo_discount_centsintegerpromo_codesarray of object- Every code the quote was priced with, in the order the buyer added them, with what each takes off. promo_code is the first and promo_discount_cents the total.
promo_codes[].codestringpromo_codes[].discount_centsintegerlinesarray of QuoteLinelines[].ticket_type_idinteger or nulllines[].product_idinteger or nulllines[].namestringlines[].quantityintegerlines[].charge_quantityinteger- How many of quantity are paid for; a group discount zeroes the free ones.
lines[].unit_price_centsintegerlines[].tax_centsintegerlines[].tax_namestringlines[].taxesarray of object- Each tax rate the line is quoted at, with its own share of
tax_cents: one entry for one rate, two for two. Absent on a line with no tax. expires_atstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nullneeds_approvalboolean- An open quote whose buyer asks for approval. The buyer's page sends it to an approver instead of converting it.
approval.approver.refstringapproval.approver.namestringapproval.submitted_atstring (RFC 3339) or nullapproval.rejected_atstring (RFC 3339) or nullapproval.po_numberstring or null- The purchase order that approved the quote, null until one is recorded. Recording it converts the quote.
approval.recorded_bystring or null- The approver who recorded the purchase order, null when the organizer recorded it or none is recorded.
approval.recorded_doorstring or null- Who recorded it:
approver(an approver on the buyer it was sent to),buyer(an approver on the quote's own buyer) ororganizer. Null until recorded. - one of
approverbuyerorganizer<nil> approval.recorded_atstring (RFC 3339) or nullbuyerobject or null- The buyer the quote is filed under, null for none: a quote made by somebody signed in, or composed for an address that is one of exactly one buyer's people. That buyer's approval applies to it.
buyer.refstringbuyer.namestringwarningobject- Only on a create's answer, and only when the quote could not be filed under a buyer it might belong to:
several_buyerswhen the email is one of the people on more than one buyer. warning.codestringwarning.messagestring
Responses
200Sent.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The quote is not open (not_open).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/quotes/H7K2PX/send \
-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/quotes/H7K2PX/send", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 12,
"ref": "2QY8FN",
"url": "https://acct.go.tito.io/annual/quotes/1qR20rFnPEV70bqeGNLJZg",
"email": "accounts@example.com",
"name": "Mary Byrne",
"org": "St Mary's",
"status": "open",
"amount_cents": 20000,
"currency": "eur",
"promo_code": "",
"promo_discount_cents": 0,
"promo_codes": [
{
"code": "SCHOOL10",
"discount_cents": 500
}
],
"locale": "en",
"lines": [
{
"charge_quantity": 2,
"kind": "ticket",
"name": "VIP",
"product_id": null,
"quantity": 2,
"tax_cents": 0,
"tax_name": "",
"ticket_type_id": 8,
"unit_price_cents": 10000
}
],
"order_ref": null,
"expires_at": "2026-09-21T22:50:00Z",
"created_at": "2026-09-07T22:50:00Z",
"needs_approval": false,
"approval": {
"approver": {
"name": "Example Ltd",
"ref": "k3m8p1"
},
"po_number": null,
"recorded_at": null,
"recorded_by": null,
"recorded_door": null,
"rejected_at": null,
"rejected_by": null,
"rejected_reason": null,
"submitted_at": "2026-09-08T09:15:00Z",
"submitted_by": "alex@example.com"
},
"buyer": {
"name": "Example Ltd Design",
"ref": "p2q7r4"
},
"warning": {
"code": "several_buyers",
"message": "example"
}
}
Remove a test order
DELETE /orders/{ref}
Delete an order that was only ever a test, as if it had never been placed — the API's copy of the admin's Remove test order. The order, its tickets, answers, emails, check-ins and history are deleted outright and its places go back on sale. One note survives (RemovedOrderNote): the ref, what the order was paid with and who removed it, which GET /orders/{ref} answers as a 410 afterwards. A paid order is refused with refund_first (409) whatever it was paid with, and nothing changes: refund or cancel it, then remove it. An order holding an extra payment that has not been refunded (see extra_payments on GET /orders/{ref}) is refused with refund_first too, whatever its status: refund that payment from the order's page in the admin first. Pending, canceled and refunded orders can be removed. A pending door sale whose card-present intent cannot be voided is refused with remove_failed (502). Send no request body; any body is ignored.
Requires orders.manage
Path parameters
refstring required- The order's ref (account-wide, not scoped to an event).
Response fields
noteobject- The one row that survives Remove test order: enough to explain a payment or refund at Stripe or PayPal that carries the order's ref.
note.refstringnote.amount_centsintegernote.currencystringnote.payment_providerstringstripe,paypalorsimulated; null for an order nobody paid for through a provider.note.payment_refstring- The provider's reference for the payment: a Stripe payment intent, a PayPal capture, or the simulated payment's own reference.
note.refund_refstring- The provider's reference for the last refund made against the order; null when there was none.
note.removed_atstring (RFC 3339)note.moved_tostring- Where the order went instead of being deleted outright:
sandboxwhen it was copied into the account's sandbox first; null for a plain delete, which is every removal this endpoint makes. note.moved_refstring- The ref the copy carries wherever it went, which is minted from its own row and can differ from
ref. Null when nothing was moved. note.invoicesarray of string- The numbers of the invoices issued for the order, oldest first. An invoice keeps its number when its order is removed, and this is where that number leads back to. Empty when none was issued.
Responses
200The order is removed and its places released.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The order is still paid, or holds an extra payment that has not been refunded (refund_first). Refund or cancel it, or refund the extra payment, then remove it.500Internal error.502A pending door sale's card-present intent could not be voided (remove_failed); the order is untouched.
curl -X DELETE 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: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"removed": true,
"ref": "H7K2PX",
"note": {
"ref": "H7K2PX",
"status": "ticket",
"amount_cents": 14900,
"currency": "eur",
"tickets": 1,
"payment_provider": "example",
"payment_ref": "H7K2PX",
"refund_ref": "H7K2PX",
"removed_at": "2026-10-14T09:00:00Z",
"removed_by": "example",
"moved_to": "example",
"moved_ref": "H7K2PX",
"invoices": [
"example"
]
}
}
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.
completed_atstring (RFC 3339) or null- When the order was finished: paid, placed by bank transfer, or made by the organizer. The moment
order.completedfired. Null until then, and null for an order finished before this field existed. 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.
items[].unit_price_centsinteger- Integer cents; see the order's currency. The ticket type's price, also on a seat that was given away free by a group discount or a free-with rule: what was charged is the matching
linesentry'stotal_cents, and itscharged_quantitysays how many were paid for. 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[].charged_quantityinteger- How many of
quantitywere paid for, never more thanquantity. Fewer when a group discount or a free-with rule gave some away, and 0 on a ticket that came free with another.total_centsisunit_price_centstimes this, plus any tax added on top;subtotal_centsis the price of allquantitybefore the free ones came off. lines[].subtotal_centsinteger- Integer cents, before any tax added on top:
unit_price_centstimesquantity, before any seats given away free (seecharged_quantity). lines[].tax_centsinteger- Integer cents. The tax snapshot taken when the line was written; 0 when the line carries no tax.
lines[].taxesarray of object- Each tax rate the line charges, with its own share of
tax_cents, which stays the line's whole tax: one entry for a line on one rate, two for a line on two (GST and QST). Absent on a line with 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.
lines[].eventstring- The slug of the event this line's ticket type belongs to, present only when that is not the order's own event. A ticket moved to another event keeps its order, and all of its money, at the event it was paid at, so the line recording the move is on that order for a type at the other event. Absent on every other line.
lines[].place_releasedboolean- True on an active line whose ticket has moved onto another ticket type or event: its money still counts, and the place it held is now held by the line carrying that ticket's ticket_ref. Absent otherwise.
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.
discounts[].on_additionboolean- True for a code typed when tickets were added to the order after it was placed. It came off that addition's own payment. Listed once the addition is paid, which is when its lines count towards
amount_cents. 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.
companyobject- Who the order was bought FOR, when the buyer ticked "I'm buying for a company or organization" at checkout. Null on an order somebody placed for themselves, which is most of them — this is the party the invoice is made out to, where
nameandemailare the person who paid. Reconcile a finance system against this, not againstname. company.vat_numberstring- As typed by the buyer, unnormalized and unverified. Empty when they did not give one — it is never required to complete a purchase.
company.address_line1string- The billing address, as one line. Empty when the buyer did not give one.
company.subdivisionstring- State, province or prefecture. Only collected for countries whose addresses carry one, so it is empty for most of Europe even when the rest of the address is filled in. Where Tito has that country's list (the US, Canada, Australia, Brazil, China, India, Indonesia and Mexico) the buyer picks from it and this is the ISO 3166-2 code, so "OH" rather than "Ohio"; elsewhere it is what they typed.
company.po_referencestring- The purchase order number the BUYER issued (BT-13), printed on the invoice. Tito's own order number is BT-14 and is
ref. company.einvoice_idstring- The country-specific routing identifier an e-invoice needs, where the buyer's country has one and the organizer turned
sends_einvoiceson. Read it againstcountry: Italy's is a Codice Destinatario (seven characters) or a PEC address, France's is the buyer's nine-digit SIREN, Germany's is a Leitweg-ID and only a public authority has one. Empty for every other country, because their invoices route onvat_number. Never format-checked at checkout: a wrong value is an invoice that gets re-sent, and refusing the sale would be worse. company.vat_statusstring- What the EU VAT register said about
vat_number. Empty until there is an answer and empty forever for a country the register does not cover. "unavailable" means we could not GET an answer, not that the number is bad: a member state's registry is down several times a month, and reading it as a verdict would act on an outage. - one of
validinvalidunavailable company.vat_registered_namestring- The register's own name for the holder of the number, where the member state returns one. It is evidence that the number belongs to
name; nothing compares the two automatically. Empty when the register gave no name, which some member states never do even for a valid number. buyerobject- What the PERSON said about themselves, as opposed to the company they may have been buying for. Null unless the organizer turned
sends_einvoiceson, which is what puts the country question on the checkout at all. buyer.countrystring- The buyer's own country as ISO 3166-1 alpha-2, upper case. Present on a company order too: with the switch on this is the only country control on the checkout, so it is the country of the purchase either way.
buyer.einvoice_idstring- The buyer's own tax number, where their country wants one on an invoice made out to a person: Italy's codice fiscale, Portugal's NIF, Spain's NIF. Empty for every other country, and always empty when
companyis set, because an invoice to a company carries the company's identifiers rather than the buyer's. PERSONAL DATA in a way a company VAT number is not — a codice fiscale encodes date of birth, sex and birthplace — so handle it like a name or an address, not like a reference code. passed_parametersobject- The allowlisted query params the buyer's link carried, kept on the order. An object rather than a list of pairs because a key appears at most once per order.
{}when the order carries none, never null. Which params are kept is the event'spassed_params_onandpassed_params_pattern. stripe_accountstring- The Stripe account (acct_…) the order's money was paid into. Its refunds go back through that account, whichever account its event pays into now. Empty for an order that never met Stripe.
cardobject- The card that paid the order, as the buyer knows it: the brand and the last four digits, read off the Stripe charge shortly after payment and printed on the receipt as "Visa ending 4242". Null where no card is recorded: an order paid by PayPal or bank transfer, a comp, a free order, or an order paid before Tito started keeping this. Never the full number, the expiry, or anything that could be charged.
card.brandstring- Stripe's own brand code, lower case:
visa,mastercard,amex,discover,diners,jcb,unionpay, and others as Stripe adds them. anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
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.
customer_confirmedobject- Who confirmed at checkout, with the code Tito emailed them, that this order is for one of the account's customers. The order's customer is that one. Null on every order nobody confirmed this way.
customer_confirmed.viastring- member: the address is on the customer's People. domain: the address is at one of the customer's email domains.
- one of
memberdomain extra_paymentsarray of OrderExtraPayment- Payments this order took and could not use, oldest first: a second payment for an order already paid, or one that arrived after it was canceled or refunded. Always an array, never null, and empty on nearly every order.
extra_payments[].providerstring- The payment provider that took the money.
- one of
stripepaypalsimulated extra_payments[].provider_idstring- The provider's own id for the payment: a Stripe PaymentIntent or a PayPal capture. Never the order's own payment.
extra_payments[].order_statusstring- What the order was when the payment arrived:
paidis a second payment for an order already paid,canceledandrefundedare a payment that arrived after the order closed. - one of
paidcanceledrefunded extra_payments[].refund_idstring- The provider's id for the refund that sent it back. Empty until it is refunded.
late_vatobject or null- VAT a company paid on a ticket with reverse charge before the EU register confirmed its VAT number, and what became of it. Null on every other order.
late_vat.statusstring- refunded: sent back to the card or PayPal account that paid, and the invoice replaced under reverse charge. reissued: a bank transfer not yet paid, whose invoice was reissued for the lower amount. to_refund: paid in a way Tito can't send money back to, so the organizer sends it and marks it refunded in the admin. by_hand: an order a discount, an addition, an agreed price or an earlier refund had already changed, for the organizer to refund from Refund. marked: the organizer marked it refunded.
- one of
refundedreissuedto_refundby_handmarked
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.410The ref belonged to a test order that was removed (removed). The body carries the note that survived it, so a payment at Stripe or PayPal carrying this ref can still be explained.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",
"completed_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,
"charged_quantity": 2,
"unit_price_cents": 14900,
"subtotal_cents": 29800,
"tax_cents": 0,
"taxes": [
{
"name": "GST",
"rate_milli_pct": 5000,
"tax_cents": 250
}
],
"total_cents": 29800,
"currency": "eur",
"status": "active",
"tier_id": 0,
"band_id": 0,
"ticket_ref": "",
"event": "",
"place_released": false
}
],
"discounts": [
{
"code": "EARLYBIRD",
"amount_cents": 2000,
"currency": "eur",
"on_addition": false
}
],
"price_adjustments": [
{
"label": "Speaker discount",
"amount_cents": -5000,
"currency": "eur"
}
],
"company": {
"name": "Analytical Engines Srl",
"country": "IT",
"vat_number": "IT02659550135",
"address_line1": "Via Ada Byron 12",
"city": "Torino",
"postcode": "10121",
"subdivision": "OH",
"po_reference": "PO-4471",
"einvoice_id": "ABC1234",
"vat_status": "valid",
"vat_registered_name": "ANALYTICAL ENGINES LTD"
},
"buyer": {
"country": "IT",
"einvoice_id": "RSSMRA85T10A562S"
},
"passed_parameters": {
"utm_campaign": "spring",
"utm_source": "newsletter"
},
"stripe_account": "acct_1MEXAMPLE",
"card": {
"brand": "visa",
"last4": "4242"
},
"anonymized_at": null,
"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"
}
]
}
],
"customer_confirmed": {
"email": "ada@example.com",
"via": "member"
},
"extra_payments": [
{
"provider": "stripe",
"provider_id": "pi_3PxExample",
"amount_cents": 29800,
"currency": "eur",
"order_status": "paid",
"received_at": "2026-05-18T14:05:40Z",
"refund_id": "",
"refunded_at": null
}
],
"late_vat": {
"status": "refunded",
"amount_cents": 14900,
"at": 1
}
}
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.
completed_atstring (RFC 3339) or null- When the order was finished: paid, placed by bank transfer, or made by the organizer. The moment
order.completedfired. Null until then, and null for an order finished before this field existed. 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.
items[].unit_price_centsinteger- Integer cents; see the order's currency. The ticket type's price, also on a seat that was given away free by a group discount or a free-with rule: what was charged is the matching
linesentry'stotal_cents, and itscharged_quantitysays how many were paid for. 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[].charged_quantityinteger- How many of
quantitywere paid for, never more thanquantity. Fewer when a group discount or a free-with rule gave some away, and 0 on a ticket that came free with another.total_centsisunit_price_centstimes this, plus any tax added on top;subtotal_centsis the price of allquantitybefore the free ones came off. lines[].subtotal_centsinteger- Integer cents, before any tax added on top:
unit_price_centstimesquantity, before any seats given away free (seecharged_quantity). lines[].tax_centsinteger- Integer cents. The tax snapshot taken when the line was written; 0 when the line carries no tax.
lines[].taxesarray of object- Each tax rate the line charges, with its own share of
tax_cents, which stays the line's whole tax: one entry for a line on one rate, two for a line on two (GST and QST). Absent on a line with 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.
lines[].eventstring- The slug of the event this line's ticket type belongs to, present only when that is not the order's own event. A ticket moved to another event keeps its order, and all of its money, at the event it was paid at, so the line recording the move is on that order for a type at the other event. Absent on every other line.
lines[].place_releasedboolean- True on an active line whose ticket has moved onto another ticket type or event: its money still counts, and the place it held is now held by the line carrying that ticket's ticket_ref. Absent otherwise.
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.
discounts[].on_additionboolean- True for a code typed when tickets were added to the order after it was placed. It came off that addition's own payment. Listed once the addition is paid, which is when its lines count towards
amount_cents. 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.
companyobject- Who the order was bought FOR, when the buyer ticked "I'm buying for a company or organization" at checkout. Null on an order somebody placed for themselves, which is most of them — this is the party the invoice is made out to, where
nameandemailare the person who paid. Reconcile a finance system against this, not againstname. company.vat_numberstring- As typed by the buyer, unnormalized and unverified. Empty when they did not give one — it is never required to complete a purchase.
company.address_line1string- The billing address, as one line. Empty when the buyer did not give one.
company.subdivisionstring- State, province or prefecture. Only collected for countries whose addresses carry one, so it is empty for most of Europe even when the rest of the address is filled in. Where Tito has that country's list (the US, Canada, Australia, Brazil, China, India, Indonesia and Mexico) the buyer picks from it and this is the ISO 3166-2 code, so "OH" rather than "Ohio"; elsewhere it is what they typed.
company.po_referencestring- The purchase order number the BUYER issued (BT-13), printed on the invoice. Tito's own order number is BT-14 and is
ref. company.einvoice_idstring- The country-specific routing identifier an e-invoice needs, where the buyer's country has one and the organizer turned
sends_einvoiceson. Read it againstcountry: Italy's is a Codice Destinatario (seven characters) or a PEC address, France's is the buyer's nine-digit SIREN, Germany's is a Leitweg-ID and only a public authority has one. Empty for every other country, because their invoices route onvat_number. Never format-checked at checkout: a wrong value is an invoice that gets re-sent, and refusing the sale would be worse. company.vat_statusstring- What the EU VAT register said about
vat_number. Empty until there is an answer and empty forever for a country the register does not cover. "unavailable" means we could not GET an answer, not that the number is bad: a member state's registry is down several times a month, and reading it as a verdict would act on an outage. - one of
validinvalidunavailable company.vat_registered_namestring- The register's own name for the holder of the number, where the member state returns one. It is evidence that the number belongs to
name; nothing compares the two automatically. Empty when the register gave no name, which some member states never do even for a valid number. buyerobject- What the PERSON said about themselves, as opposed to the company they may have been buying for. Null unless the organizer turned
sends_einvoiceson, which is what puts the country question on the checkout at all. buyer.countrystring- The buyer's own country as ISO 3166-1 alpha-2, upper case. Present on a company order too: with the switch on this is the only country control on the checkout, so it is the country of the purchase either way.
buyer.einvoice_idstring- The buyer's own tax number, where their country wants one on an invoice made out to a person: Italy's codice fiscale, Portugal's NIF, Spain's NIF. Empty for every other country, and always empty when
companyis set, because an invoice to a company carries the company's identifiers rather than the buyer's. PERSONAL DATA in a way a company VAT number is not — a codice fiscale encodes date of birth, sex and birthplace — so handle it like a name or an address, not like a reference code. passed_parametersobject- The allowlisted query params the buyer's link carried, kept on the order. An object rather than a list of pairs because a key appears at most once per order.
{}when the order carries none, never null. Which params are kept is the event'spassed_params_onandpassed_params_pattern. stripe_accountstring- The Stripe account (acct_…) the order's money was paid into. Its refunds go back through that account, whichever account its event pays into now. Empty for an order that never met Stripe.
cardobject- The card that paid the order, as the buyer knows it: the brand and the last four digits, read off the Stripe charge shortly after payment and printed on the receipt as "Visa ending 4242". Null where no card is recorded: an order paid by PayPal or bank transfer, a comp, a free order, or an order paid before Tito started keeping this. Never the full number, the expiry, or anything that could be charged.
card.brandstring- Stripe's own brand code, lower case:
visa,mastercard,amex,discover,diners,jcb,unionpay, and others as Stripe adds them. anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
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",
"completed_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,
"charged_quantity": 2,
"unit_price_cents": 14900,
"subtotal_cents": 29800,
"tax_cents": 0,
"taxes": [
{
"name": "GST",
"rate_milli_pct": 5000,
"tax_cents": 250
}
],
"total_cents": 29800,
"currency": "eur",
"status": "active",
"tier_id": 0,
"band_id": 0,
"ticket_ref": "",
"event": "",
"place_released": false
}
],
"discounts": [
{
"code": "EARLYBIRD",
"amount_cents": 2000,
"currency": "eur",
"on_addition": false
}
],
"price_adjustments": [
{
"label": "Speaker discount",
"amount_cents": -5000,
"currency": "eur"
}
],
"company": {
"name": "Analytical Engines Srl",
"country": "IT",
"vat_number": "IT02659550135",
"address_line1": "Via Ada Byron 12",
"city": "Torino",
"postcode": "10121",
"subdivision": "OH",
"po_reference": "PO-4471",
"einvoice_id": "ABC1234",
"vat_status": "valid",
"vat_registered_name": "ANALYTICAL ENGINES LTD"
},
"buyer": {
"country": "IT",
"einvoice_id": "RSSMRA85T10A562S"
},
"passed_parameters": {
"utm_campaign": "spring",
"utm_source": "newsletter"
},
"stripe_account": "acct_1MEXAMPLE",
"card": {
"brand": "visa",
"last4": "4242"
},
"anonymized_at": null
}
Anonymize an order's buyer and attendees
POST /orders/{ref}/anonymize
Remove the buyer from an order, with their name, email address, phone number, company and billing address, and the person from every ticket on it, with every answer and every file sent with the answers. A customer record the order is linked to (see GET /buyers/{ref}) is not changed and is named in kept.customer; anonymize it with POST /buyers/{ref}/anonymize. It happens at once and cannot be undone, the API's copy of the admin's Anonymize confirm step, which runs the same code. What stays: the record itself with its reference, status and place in every count, what was bought and paid and when, the payment reference, and every invoice, receipt and credit note already issued, exactly as issued. Copies of the person in sent emails, the record's history, its webhook deliveries, workflow runs and the API request log are cleared too. What Tito cannot reach is named in outside_tito: the payment provider's own record, connected services the person was sent to, and whatever your webhooks already delivered. Every call writes a line to the data-protection log (GET /data-protection-log) that names the record's reference and never the person. The link tags its orders arrived with go too, ad click ids and your own tags included, except utm_* campaign tags, which stay so the order still counts toward the link that sold it. Send no request body; any body is ignored. An order still waiting for a bank transfer is canceled first, so a payment that arrives later cannot fulfil it; one whose tickets were already issued is left as it is.
Requires erasure
Path parameters
refstring required- The order's ref (account-wide, not scoped to an event).
Response fields
refstring- The record's reference: a ticket, order, customer, waitlist entry or ticket request ref, or the invitation's id.
anonymized_atstring (RFC 3339)removedobjectremoved.emailsinteger- Sent emails whose address and words were removed. The record that each was sent stays.
removed.link_tagsinteger- Orders whose link tags were removed: every tag that isn't a
utm_*campaign tag, ad click ids included. removed.interestboolean- Their email address was taken off the mailing list, where they signed up to hear when tickets go on sale.
removed.cardboolean- A card saved with a ticket request this withdrew, which Stripe was asked to delete.
keptobjectkept.other_recordsinteger- Other orders, tickets and invitations with the same email address, which are not changed.
kept.customerstring or null- The customer record an anonymized order is linked to, which is not changed.
kept.campaign_tagsinteger- Orders whose
utm_*campaign tags were kept, so they still count toward the link that sold them. kept.orderstring or null- The order a waitlist place was taken with, or that a ticket request's invitation was claimed with, which is not changed.
kept.invitationinteger or null- The invitation a ticket request was approved into, which is not changed.
outside_titoobjectoutside_tito.payment_providerstring or null- The provider that keeps its own record of the payment, with the buyer's name and email address.
- one of
StripePayPal<nil> outside_tito.servicesarray of string- Connected services the person's details were sent to, which keep their own copy.
outside_tito.cardboolean- A card saved with a ticket request that was already decided, which Stripe keeps.
Responses
200The buyer and attendees are anonymized.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The person on this record has already been anonymized (already_anonymized). Nothing changes.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/anonymize \
-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/anonymize", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"anonymized": true,
"kind": "ticket",
"ref": "H7K2PX",
"anonymized_at": "2026-10-14T09:00:00Z",
"removed": {
"tickets": 1,
"orders": 1,
"quotes": 1,
"answers": 1,
"files": 1,
"emails": 1,
"contact": true,
"waitlist": true,
"ticket_request": true,
"link_tags": 1,
"interest": true,
"card": true
},
"kept": {
"documents": 1,
"other_records": 1,
"customer": "example",
"campaign_tags": 1,
"order": "H7K2PX",
"ticket": "T9QW4M",
"invitation": 1
},
"outside_tito": {
"payment_provider": "Stripe",
"services": [
"example"
],
"webhooks": true,
"card": true
}
}
Cancel an order
POST /orders/{ref}/cancel
POST /orders/{ref}/cancel cancels only a pending order. A non-pending order returns 409 not_pending. Before cancellation, an existing Stripe Checkout session must be confirmed expired. A card-reader or bank-payment PaymentIntent must be canceled or confirmed already canceled.
A recorded unknown outcome, in-flight capture, conflict with another payment attempt, or failed final state check returns 409 cancel_failed. The refusal does not itself cancel the order or release its reservations. Other errors while preparing cancellation, including provider request failures, return 502 cancel_failed. An earlier check may already have stopped a payment attempt. On success, cancellation releases held inventory and returns {"canceled":true,"ref":"..."}.
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.409A non-pending order returns409 not_pending. A recorded unknown outcome, in-flight capture, conflict with another payment attempt, or failed final state check returns409 cancel_failed.500Internal error.502Other errors while preparing cancellation, including provider request failures, return502 cancel_failed. An earlier check may already have stopped a payment attempt.
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"
}
Resend an order confirmation
POST /orders/{ref}/confirmation
Sends the buyer their order confirmation again and returns mail_status, to, and ref. mail_status is queued when the email carries a PDF: the order's receipt or invoice, or its tickets when the event attaches them (which a free order's confirmation can carry too). It goes out within five minutes, with each PDF that could be made by then. With no body it is the confirmation as it stands; a body may change its subject and inbox preview line, add a note under the greeting and choose the sender it goes from (email_sender_id), as the admin's Resend order confirmation screen does. An order that is not paid answers 409 not_paid, one with no address to send to answers 409 no_email, and a second resend to the same address within two minutes of the first answers 409 recently_sent and sends nothing.
Requires orders.manage
Path parameters
refstring required- The order's ref (account-wide, not scoped to an event).
Request body
notestring- Under the greeting, above the rest of the email, in the message composer's source: plain text with a blank line between paragraphs,
{{ mark.bold }}style marks for formatting, and details such as{{ person.first_name }}or{{ order.reference }}, resolved for the buyer. Up to 2000 characters; a longer note is refused with422 note_too_long. It goes with this send only. email_sender_address_idintegeremail_sender_address_idis optional. A positive ID selects that exact address from theaddressesreturned byGET /email-senders. Its sender must be live; a saved message can keep its unchanged choice while the sender is pending. If both IDs are positive, the address must belong toemail_sender_id, or the request returns HTTP 422invalid_body. Address ID0follows the event unless a positiveemail_sender_idselects a domain. Omitting both fields from a messagePATCHpreserves its choice. A domain-onlyPATCHrepeating the current domain also preserves a selected address. To select the domain explicitly, send its ID with address ID0.email_sender_idinteger- The sender to send from: one of the account's live senders (
GET /email-senders). 0 or left out sends from the event's sender. Any other id is refused with422 invalid_body. A chosen sender that stops being live before the email goes is replaced by the event's sender.
Response fields
Responses
200The confirmation was handed to the mail transport.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The order is not paid (not_paid), has no address to send to (no_email), or was sent this confirmation within the last two minutes (recently_sent).422The body is not the expected shape, oremail_sender_idis not one of the account's live senders (invalid_body), or the note is longer than 2000 characters (note_too_long).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/confirmation \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"subject":"Your tickets for Annual Conference"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/confirmation", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"subject": "Your tickets for Annual Conference"
}),
});
const data = await res.json();
{
"ref": "H7K2PX",
"mail_status": "sent",
"to": "example"
}
Extend an invoice's due date
POST /orders/{ref}/due-date
Give the buyer of an order awaiting a bank transfer more time to pay. due_date is a day on the event's own clock, and the invoice is due at the end of it. It must be after today and after the date the invoice is due now. The invoice and the buyer's order page show the new date, the payment reminders start again from it, and so does the count towards automatic cancelling; the reminders already sent stay in the order's history. The buyer isn't emailed. The places a capped add-on or timed session holds are kept until the new date. If the invoice is already overdue, those places were released at the old due date, and if some have been booked since, the call answers 409 places_gone and nothing changes. An order with no invoice awaiting payment answers 409 not_awaiting.
Requires orders.manage
Path parameters
refstring required- The order's ref (account-wide, not scoped to an event).
Request body
due_datestring required- The new due date, a day on the event's clock. The invoice is due at the end of it.
Response fields
Responses
200The invoice's due date has moved.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The order has no invoice awaiting payment (not_awaiting), or the invoice is overdue and some of the places it held have been booked since (places_gone).422The body isn't{"due_date":"YYYY-MM-DD"}(invalid_body), or the date isn't after both today and the current due date (due_date_too_early).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/due-date \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"due_date":"2026-10-14T09:00:00Z"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/due-date", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"due_date": "2026-10-14T09:00:00Z"
}),
});
const data = await res.json();
{
"ref": "H7K2PX",
"due_date": "2026-10-14T09:00:00Z",
"due_at": "2026-10-14T09:00:00Z"
}
Update an order's invoice details
POST /orders/{ref}/invoice-details
Change who an order's invoice is made out to: the company name, VAT number, billing address, the buyer's own reference and purchase order number. Send only the fields that change; an absent field keeps what the invoice says now, and an empty string clears it. If the order already has an issued invoice, it's canceled with a credit note and an invoice with the new details is issued under a new number, both listed on the order. If none has been issued yet, the first one carries the new details. The amounts never change: a new VAT number changes who the invoice is addressed to, not the tax on it. A new VAT number is checked against the EU register again, and a new country drops the country-specific invoicing identifier. email_buyer sends the buyer an email naming the canceled invoice, its credit note and the new invoice; the default is not to. Works on orders that are paid or awaiting payment; anything else answers 409 not_updatable.
Requires orders.manage
Path parameters
refstring required- The order's ref (account-wide, not scoped to an event).
Request body
buyer_referencestring- A code the buyer's organization uses to route invoices, such as Germany's Leitweg-ID.
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.
completed_atstring (RFC 3339) or null- When the order was finished: paid, placed by bank transfer, or made by the organizer. The moment
order.completedfired. Null until then, and null for an order finished before this field existed. 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.
items[].unit_price_centsinteger- Integer cents; see the order's currency. The ticket type's price, also on a seat that was given away free by a group discount or a free-with rule: what was charged is the matching
linesentry'stotal_cents, and itscharged_quantitysays how many were paid for. 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[].charged_quantityinteger- How many of
quantitywere paid for, never more thanquantity. Fewer when a group discount or a free-with rule gave some away, and 0 on a ticket that came free with another.total_centsisunit_price_centstimes this, plus any tax added on top;subtotal_centsis the price of allquantitybefore the free ones came off. lines[].subtotal_centsinteger- Integer cents, before any tax added on top:
unit_price_centstimesquantity, before any seats given away free (seecharged_quantity). lines[].tax_centsinteger- Integer cents. The tax snapshot taken when the line was written; 0 when the line carries no tax.
lines[].taxesarray of object- Each tax rate the line charges, with its own share of
tax_cents, which stays the line's whole tax: one entry for a line on one rate, two for a line on two (GST and QST). Absent on a line with 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.
lines[].eventstring- The slug of the event this line's ticket type belongs to, present only when that is not the order's own event. A ticket moved to another event keeps its order, and all of its money, at the event it was paid at, so the line recording the move is on that order for a type at the other event. Absent on every other line.
lines[].place_releasedboolean- True on an active line whose ticket has moved onto another ticket type or event: its money still counts, and the place it held is now held by the line carrying that ticket's ticket_ref. Absent otherwise.
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.
discounts[].on_additionboolean- True for a code typed when tickets were added to the order after it was placed. It came off that addition's own payment. Listed once the addition is paid, which is when its lines count towards
amount_cents. 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.
companyobject- Who the order was bought FOR, when the buyer ticked "I'm buying for a company or organization" at checkout. Null on an order somebody placed for themselves, which is most of them — this is the party the invoice is made out to, where
nameandemailare the person who paid. Reconcile a finance system against this, not againstname. company.vat_numberstring- As typed by the buyer, unnormalized and unverified. Empty when they did not give one — it is never required to complete a purchase.
company.address_line1string- The billing address, as one line. Empty when the buyer did not give one.
company.subdivisionstring- State, province or prefecture. Only collected for countries whose addresses carry one, so it is empty for most of Europe even when the rest of the address is filled in. Where Tito has that country's list (the US, Canada, Australia, Brazil, China, India, Indonesia and Mexico) the buyer picks from it and this is the ISO 3166-2 code, so "OH" rather than "Ohio"; elsewhere it is what they typed.
company.po_referencestring- The purchase order number the BUYER issued (BT-13), printed on the invoice. Tito's own order number is BT-14 and is
ref. company.einvoice_idstring- The country-specific routing identifier an e-invoice needs, where the buyer's country has one and the organizer turned
sends_einvoiceson. Read it againstcountry: Italy's is a Codice Destinatario (seven characters) or a PEC address, France's is the buyer's nine-digit SIREN, Germany's is a Leitweg-ID and only a public authority has one. Empty for every other country, because their invoices route onvat_number. Never format-checked at checkout: a wrong value is an invoice that gets re-sent, and refusing the sale would be worse. company.vat_statusstring- What the EU VAT register said about
vat_number. Empty until there is an answer and empty forever for a country the register does not cover. "unavailable" means we could not GET an answer, not that the number is bad: a member state's registry is down several times a month, and reading it as a verdict would act on an outage. - one of
validinvalidunavailable company.vat_registered_namestring- The register's own name for the holder of the number, where the member state returns one. It is evidence that the number belongs to
name; nothing compares the two automatically. Empty when the register gave no name, which some member states never do even for a valid number. buyerobject- What the PERSON said about themselves, as opposed to the company they may have been buying for. Null unless the organizer turned
sends_einvoiceson, which is what puts the country question on the checkout at all. buyer.countrystring- The buyer's own country as ISO 3166-1 alpha-2, upper case. Present on a company order too: with the switch on this is the only country control on the checkout, so it is the country of the purchase either way.
buyer.einvoice_idstring- The buyer's own tax number, where their country wants one on an invoice made out to a person: Italy's codice fiscale, Portugal's NIF, Spain's NIF. Empty for every other country, and always empty when
companyis set, because an invoice to a company carries the company's identifiers rather than the buyer's. PERSONAL DATA in a way a company VAT number is not — a codice fiscale encodes date of birth, sex and birthplace — so handle it like a name or an address, not like a reference code. passed_parametersobject- The allowlisted query params the buyer's link carried, kept on the order. An object rather than a list of pairs because a key appears at most once per order.
{}when the order carries none, never null. Which params are kept is the event'spassed_params_onandpassed_params_pattern. stripe_accountstring- The Stripe account (acct_…) the order's money was paid into. Its refunds go back through that account, whichever account its event pays into now. Empty for an order that never met Stripe.
cardobject- The card that paid the order, as the buyer knows it: the brand and the last four digits, read off the Stripe charge shortly after payment and printed on the receipt as "Visa ending 4242". Null where no card is recorded: an order paid by PayPal or bank transfer, a comp, a free order, or an order paid before Tito started keeping this. Never the full number, the expiry, or anything that could be charged.
card.brandstring- Stripe's own brand code, lower case:
visa,mastercard,amex,discover,diners,jcb,unionpay, and others as Stripe adds them. anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
Responses
200The order, in the same shapeGET /orders/{ref}answers with.companyshows what the invoice is now made out to.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The order is canceled, refunded or anonymized (not_updatable).422The body isn't an object of the fields below (invalid_body), the VAT number has a character a VAT number can't contain (invalid_vat_number), or the country isn't an ISO 3166-1 alpha-2 code (invalid_country).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/invoice-details \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"bill_to":"example"}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/invoice-details", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"bill_to": "example"
}),
});
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",
"completed_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,
"charged_quantity": 2,
"unit_price_cents": 14900,
"subtotal_cents": 29800,
"tax_cents": 0,
"taxes": [
{
"name": "GST",
"rate_milli_pct": 5000,
"tax_cents": 250
}
],
"total_cents": 29800,
"currency": "eur",
"status": "active",
"tier_id": 0,
"band_id": 0,
"ticket_ref": "",
"event": "",
"place_released": false
}
],
"discounts": [
{
"code": "EARLYBIRD",
"amount_cents": 2000,
"currency": "eur",
"on_addition": false
}
],
"price_adjustments": [
{
"label": "Speaker discount",
"amount_cents": -5000,
"currency": "eur"
}
],
"company": {
"name": "Analytical Engines Srl",
"country": "IT",
"vat_number": "IT02659550135",
"address_line1": "Via Ada Byron 12",
"city": "Torino",
"postcode": "10121",
"subdivision": "OH",
"po_reference": "PO-4471",
"einvoice_id": "ABC1234",
"vat_status": "valid",
"vat_registered_name": "ANALYTICAL ENGINES LTD"
},
"buyer": {
"country": "IT",
"einvoice_id": "RSSMRA85T10A562S"
},
"passed_parameters": {
"utm_campaign": "spring",
"utm_source": "newsletter"
},
"stripe_account": "acct_1MEXAMPLE",
"card": {
"brand": "visa",
"last4": "4242"
},
"anonymized_at": null
}
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 Tito collected nothing for it, as on an order marked paid by hand (no_payment_intent): return the money the way you were paid, then cancel its tickets withPOST /tickets/{ref}/cancel, which needsorders.manageand says why for a ticket it can't cancel (one from an invitation, a portal or a bundle).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
}
Reinstate a canceled order
POST /orders/{ref}/reinstate
Put a canceled order back the way it was. The order returns to paid, the tickets the cancellation voided start working again, and the places they hold come off sale. Nothing here moves money: no charge is made and no refund is sent, so an order whose money was returned by hand has to be collected again the same way. Only an order that reached canceled from paid can be put back — anything else answers 409 not_reinstatable, which covers an order canceled before it was ever paid, an order a buyer edit replaced with another, an invitation's order (reinstated from the invitation instead), and an order whose tickets were each refunded or canceled on their own before it was. Tickets taken off the order individually stay off, because their share may already have been refunded. Putting an order back is a fresh demand on capacity, and the overshoot is reported rather than refused: if the places have been sold since, the call still goes through and oversold in the answer says how far past the cap the ticket type now is.
Requires orders.manage
Path parameters
refstring required- The order's ref (account-wide, not scoped to an event).
Request body
email_buyerboolean- Email the buyer to say their order is back on and their tickets work again. Off unless asked for.
Response fields
oversoldinteger- How far past its cap the ticket type now is, and 0 when the places were there. A reinstate is never refused for capacity, so this is where a caller finds out it went over.
Responses
200The order is back and its tickets work again.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The order cannot be put back (not_reinstatable).422The request body could not be read (invalid_body).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/reinstate \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"email_buyer":true}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/orders/H7K2PX/reinstate", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"email_buyer": true
}),
});
const data = await res.json();
{
"reinstated": true,
"ref": "H7K2PX",
"tickets": 1,
"oversold": 42
}
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[].confirmed_atstring (RFC 3339) or null- When the attendee said they are still coming, by pressing an "I'm still coming" button in a message. Null until they do.
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.
tickets[].duplicateboolean- True when this ticket is valid and its attendee's own email address, ignoring case and surrounding spaces, is on valid tickets in two or more different orders at this event. Tickets in one order sharing an address are not duplicates, and a ticket with no attendee email of its own is never one. The admin attendee list marks the same tickets Duplicate.
tickets[].anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
tickets[].moved_fromobject or null- Where this ticket was before an organizer moved it to this event, null for a ticket that has never moved. A moved ticket keeps its reference, and its order stays at the event it was paid at, so order_ref names an order at that event. A ticket moved more than once says where it came from most recently.
tickets[].moved_from.eventstring- The slug of the event it left. Empty if that event has since been deleted.
tickets[].moved_from.ticket_typestring- The name of the ticket type it held there, as it was when it moved.
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",
"confirmed_at": null,
"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"
}
],
"duplicate": false,
"anonymized_at": null,
"moved_from": null
}
],
"next_after": "example"
}
Move tickets to another event
POST /events/{slug}/tickets/move
Moves several tickets from this event to another upcoming event in this account, the admin's Move to another event from the attendee list. Each ticket keeps its reference, and its old link takes the holder to the new event. The order and all its money stay at this event, where it was paid. The people count at the new event.
Nothing here moves money: no charge is made and no refund is sent, whatever the two ticket types cost. To charge or refund the difference for one person, move them from their own page in the admin.
ticket_types maps each ticket type these tickets hold now, by id, to a ticket type on sale at to_event. Every type among the tickets that will move needs an answer, or the call answers 422 unmapped_ticket_type naming the ones without, and nothing moves. to_event has to be another upcoming, published event in the same currency as this one. One in another currency answers 409 other_currency.
A ticket that can't move stays where it is and is listed under staying with the reason: void (canceled), invitation or portal (issued by one), bundle (one leg of a bundle), extras (it holds a time or an add-on only this event has), waiting (an earlier move is waiting on its payment link) and currency (its order was paid in another currency). The rest move. A ticket type already full at the new event still takes them, past its limit.
notify (default false) emails each moved attendee their ticket for the new event, which also takes the old event out of their calendar.
Requires orders.manage
Path parameters
slugstring required- The event's slug.
Request body
ticket_typesobject required- Each ticket type id these tickets hold now, as a string key, to the id of a ticket type at
to_event.
Response fields
staying[].refstringunknownarray of string- The refs given that aren't tickets at this event, so nothing was done with them.
failedinteger- How many tickets could have moved and didn't, because something went wrong. They are still at this event.
Responses
200Who moved, and who stayed where they are.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409to_eventsells in another currency (other_currency).422The body is missing something,to_eventisn't an event they can move to (invalid_event), or a ticket type has no answer (unmapped_ticket_type).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/tickets/move \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"to_event":"example","tickets":["T9QW4M"],"ticket_types":null}'
// 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/move", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"to_event": "example",
"tickets": [
"T9QW4M"
],
"ticket_types": null
}),
});
const data = await res.json();
{
"to_event": "example",
"moved": [
"example"
],
"staying": [
{
"ref": "H7K2PX",
"reason": "void"
}
],
"unknown": [
"example"
],
"failed": 1
}
Read a ticket
GET /events/{slug}/tickets/{ref}
Returns the ticket in the same shape as the ticket list, including answers and external sync status. admin_url links to the attendee in the admin interface. The ticket must belong to the event in the URL or the API returns 404.
Requires attendees.view
Path parameters
slugstring requiredrefstring required
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.
confirmed_atstring (RFC 3339) or null- When the attendee said they are still coming, by pressing an "I'm still coming" button in a message. Null until they do.
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.
duplicateboolean- True when this ticket is valid and its attendee's own email address, ignoring case and surrounding spaces, is on valid tickets in two or more different orders at this event. Tickets in one order sharing an address are not duplicates, and a ticket with no attendee email of its own is never one. The admin attendee list marks the same tickets Duplicate.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
moved_fromobject or null- Where this ticket was before an organizer moved it to this event, null for a ticket that has never moved. A moved ticket keeps its reference, and its order stays at the event it was paid at, so order_ref names an order at that event. A ticket moved more than once says where it came from most recently.
admin_urlstring
Responses
200Read a ticket401No 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/tickets/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/tickets/H7K2PX", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
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",
"confirmed_at": null,
"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"
}
],
"duplicate": false,
"anonymized_at": null,
"moved_from": null,
"admin_url": "https://example.com/hooks/tito"
}
Make links to a ticket
POST /events/{slug}/tickets/{ref}/links
Makes a link to one ticket's own page, to send to whoever holds it. url is a fresh link that stops working at expires_at; direct_url is the ticket's permanent page. Both open the ticket without signing in, so send them only to the ticket's holder.
Requires orders.manage
Path parameters
slugstring required- The event's slug.
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
Responses
201401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/tickets/H7K2PX/links \
-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/H7K2PX/links", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"direct_url": "https://example.com/hooks/tito",
"url": "https://example.com/hooks/tito",
"expires_at": "2026-10-14T09:00:00Z"
}
Reschedule a ticket
POST /events/{slug}/tickets/{ref}/reschedule
Send seat_id, session_id, and optional notify, which defaults to false. The API moves the capacity hold and keeps the ticket reference. It accepts closed, full, or clashing targets as the admin interface does. Check the warnings returned by the times endpoint before making the change. An invalid choice returns 422, a stale choice returns 409, and a void ticket returns 409. The response includes ticket_ref, seat_id, session_id, and mail_status. If notification fails, mail_status is failed and the move remains successful. A ticket without an eligible email recipient returns mail_status: no_recipient. mail_status is the attendee's email only. When somebody else booked the ticket and the move left the old option in their calendar, notify also sends that booker a short note, reported in booker_mail_status (sent, logged, or failed) with the address in booker_email. booker_mail_status is not_requested without notify, and not_sent when the booker was owed no note. booker_email is empty unless a note was sent.
Requires orders.manage
Path parameters
slugstring requiredrefstring required
Request body
Response fields
Responses
200Reschedule a ticket401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Request refused422Request refused500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/tickets/H7K2PX/reschedule \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"seat_id":1,"session_id":1}'
// 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/H7K2PX/reschedule", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"seat_id": 1,
"session_id": 1
}),
});
const data = await res.json();
{
"ticket_ref": "T9QW4M",
"seat_id": 1,
"session_id": 1,
"mail_status": "ticket",
"booker_mail_status": "not_requested",
"booker_email": "ada@example.com"
}
Resend an assignment email
POST /events/{slug}/tickets/{ref}/resend
Sends the assignment email and returns mail_status, to, and ticket_ref. mail_status is queued when the event attaches the ticket as a PDF: it goes out within five minutes, with the PDF if it could be made by then. With no body it is the ticket email as it stands; a body may change its subject and inbox preview line, add a note under the greeting and choose the sender it goes from (email_sender_id), as the admin's Send ticket email screen does. A void, anonymous, unassigned, or email-less ticket returns 409, and so does a second resend to the same address within two minutes of the first (recently_sent), which sends nothing. No email preview is returned.
Requires orders.manage
Path parameters
slugstring requiredrefstring required
Request body
notestring- Under the greeting, above the rest of the email, in the message composer's source: plain text with a blank line between paragraphs,
{{ mark.bold }}style marks for formatting, and details such as{{ person.first_name }}or{{ ticket.type }}, resolved for the ticket's holder. Up to 2000 characters; a longer note is refused with422 note_too_long. It goes with this send only. email_sender_address_idintegeremail_sender_address_idis optional. A positive ID selects that exact address from theaddressesreturned byGET /email-senders. Its sender must be live; a saved message can keep its unchanged choice while the sender is pending. If both IDs are positive, the address must belong toemail_sender_id, or the request returns HTTP 422invalid_body. Address ID0follows the event unless a positiveemail_sender_idselects a domain. Omitting both fields from a messagePATCHpreserves its choice. A domain-onlyPATCHrepeating the current domain also preserves a selected address. To select the domain explicitly, send its ID with address ID0.email_sender_idinteger- The sender to send from: one of the account's live senders (
GET /email-senders). 0 or left out sends from the event's sender. Any other id is refused with422 invalid_body. A chosen sender that stops being live before the email goes is replaced by the event's sender.
Response fields
Responses
200Resend an assignment email401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409Void, anonymous, unassigned or email-less (not_sendable), or sent to this address within the last two minutes (recently_sent).422Request refused500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/tickets/H7K2PX/resend \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"subject":"Your ticket for Annual Conference"}'
// 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/H7K2PX/resend", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"subject": "Your ticket for Annual Conference"
}),
});
const data = await res.json();
{
"ticket_ref": "T9QW4M",
"mail_status": "sent",
"to": "example"
}
Read timed allocations
GET /events/{slug}/tickets/{ref}/times
Returns the event timezone, blocked_reason, and each seat's current allocation and target times. Each seat includes id, entitlement_id, entitlement_name, units, current, and targets. Each time includes id, name, starts_at, ends_at, available, full, short, closed, and clash. Dates use the RFC 3339 date and time format. available: -1 means unlimited.
Requires attendees.view
Path parameters
slugstring requiredrefstring required
Response fields
ticket_refstringtimezonestringblocked_reasonstringseatsarray of AttendeeRescheduleSeatseats[].idintegerseats[].entitlement_idintegerseats[].entitlement_namestringseats[].unitsintegerseats[].currentobjectseats[].current.idintegerseats[].current.availableintegerseats[].current.namestringseats[].current.starts_atstringseats[].current.ends_atstringseats[].current.fullbooleanseats[].current.shortbooleanseats[].current.closedbooleanseats[].current.clashbooleanseats[].targetsarray of AttendeeRescheduleTimeseats[].targets[].idintegerseats[].targets[].availableintegerseats[].targets[].namestringseats[].targets[].starts_atstringseats[].targets[].ends_atstringseats[].targets[].fullbooleanseats[].targets[].shortbooleanseats[].targets[].closedbooleanseats[].targets[].clashboolean
Responses
200Read timed allocations401No 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/tickets/H7K2PX/times \
-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/H7K2PX/times", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"ticket_ref": "T9QW4M",
"timezone": "Europe/Dublin",
"blocked_reason": "example",
"seats": [
{
"id": 1,
"entitlement_id": 1,
"entitlement_name": "Ada Lovelace",
"units": 1,
"current": {
"id": 1,
"available": 1,
"name": "Ada Lovelace",
"starts_at": "2026-10-14T09:00:00Z",
"ends_at": "2026-10-14T09:00:00Z",
"full": true,
"short": true,
"closed": true,
"clash": true
},
"targets": [
{
"id": 1,
"available": 1,
"name": "Ada Lovelace",
"starts_at": "2026-10-14T09:00:00Z",
"ends_at": "2026-10-14T09:00:00Z",
"full": true,
"short": true,
"closed": true,
"clash": true
}
]
}
]
}
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.
confirmed_atstring (RFC 3339) or null- When the attendee said they are still coming, by pressing an "I'm still coming" button in a message. Null until they do.
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.
duplicateboolean- True when this ticket is valid and its attendee's own email address, ignoring case and surrounding spaces, is on valid tickets in two or more different orders at this event. Tickets in one order sharing an address are not duplicates, and a ticket with no attendee email of its own is never one. The admin attendee list marks the same tickets Duplicate.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
moved_fromobject or null- Where this ticket was before an organizer moved it to this event, null for a ticket that has never moved. A moved ticket keeps its reference, and its order stays at the event it was paid at, so order_ref names an order at that event. A ticket moved more than once says where it came from most recently.
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",
"confirmed_at": null,
"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"
}
],
"duplicate": false,
"anonymized_at": null,
"moved_from": null
}
Anonymize an attendee
POST /tickets/{ref}/anonymize
Remove the person from one ticket: their name, email address, phone number, company and job title, and every answer they gave. It happens at once and cannot be undone, the API's copy of the admin's Anonymize confirm step, which runs the same code. What stays: the record itself with its reference, status and place in every count, what was bought and paid and when, the payment reference, and every invoice, receipt and credit note already issued, exactly as issued. Copies of the person in sent emails, the record's history, its webhook deliveries, workflow runs and the API request log are cleared too. What Tito cannot reach is named in outside_tito: the payment provider's own record, connected services the person was sent to, and whatever your webhooks already delivered. Every call writes a line to the data-protection log (GET /data-protection-log) that names the record's reference and never the person. Send no request body; any body is ignored.
Requires erasure
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
refstring- The record's reference: a ticket, order, customer, waitlist entry or ticket request ref, or the invitation's id.
anonymized_atstring (RFC 3339)removedobjectremoved.emailsinteger- Sent emails whose address and words were removed. The record that each was sent stays.
removed.link_tagsinteger- Orders whose link tags were removed: every tag that isn't a
utm_*campaign tag, ad click ids included. removed.interestboolean- Their email address was taken off the mailing list, where they signed up to hear when tickets go on sale.
removed.cardboolean- A card saved with a ticket request this withdrew, which Stripe was asked to delete.
keptobjectkept.other_recordsinteger- Other orders, tickets and invitations with the same email address, which are not changed.
kept.customerstring or null- The customer record an anonymized order is linked to, which is not changed.
kept.campaign_tagsinteger- Orders whose
utm_*campaign tags were kept, so they still count toward the link that sold them. kept.orderstring or null- The order a waitlist place was taken with, or that a ticket request's invitation was claimed with, which is not changed.
kept.invitationinteger or null- The invitation a ticket request was approved into, which is not changed.
outside_titoobjectoutside_tito.payment_providerstring or null- The provider that keeps its own record of the payment, with the buyer's name and email address.
- one of
StripePayPal<nil> outside_tito.servicesarray of string- Connected services the person's details were sent to, which keep their own copy.
outside_tito.cardboolean- A card saved with a ticket request that was already decided, which Stripe keeps.
Responses
200The attendee is anonymized.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The person on this record has already been anonymized (already_anonymized). Nothing changes.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/tickets/H7K2PX/anonymize \
-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/anonymize", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"anonymized": true,
"kind": "ticket",
"ref": "H7K2PX",
"anonymized_at": "2026-10-14T09:00:00Z",
"removed": {
"tickets": 1,
"orders": 1,
"quotes": 1,
"answers": 1,
"files": 1,
"emails": 1,
"contact": true,
"waitlist": true,
"ticket_request": true,
"link_tags": 1,
"interest": true,
"card": true
},
"kept": {
"documents": 1,
"other_records": 1,
"customer": "example",
"campaign_tags": 1,
"order": "H7K2PX",
"ticket": "T9QW4M",
"invitation": 1
},
"outside_tito": {
"payment_provider": "Stripe",
"services": [
"example"
],
"webhooks": true,
"card": true
}
}
Cancel a ticket without a refund
POST /tickets/{ref}/cancel
Stop one ticket working and put its place back on sale. Nothing here moves money: the ticket's share of the order stays with you, and no refund is sent. To send the money back as well, use POST /tickets/{ref}/refund. Nothing is emailed.
It answers the same refusals as the admin's Refund or cancel page, with 409 not_cancelable and the reason: a ticket on an order that isn't paid (not_paid), one issued by an invitation (invitation) or a portal (portal), and one leg of a bundle (bundle). A ticket that is already canceled answers 200 with already: true, so a retry is safe. No request body.
The admin's attendee list cancels several tickets at once with this same act. Over the API, call this once per ticket.
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.
Response fields
Responses
200The ticket no longer works.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The ticket cannot be canceled here (not_cancelable).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/tickets/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/tickets/H7K2PX/cancel", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"canceled": true,
"ref": "H7K2PX",
"already": true
}
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 Tito collected nothing for it, as on an order marked paid by hand (no_payment_intent): return the money the way you were paid, then cancel it withPOST /tickets/{ref}/cancel, which needsorders.manageand says why if it can't cancel it. A ticket Tito did take money for, on an account whose payment provider has since been disconnected, is 503payments_unavailable.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
}
Reinstate a canceled ticket
POST /tickets/{ref}/reinstate
Put one canceled ticket back. The ticket works again, its share of the order's money returns to the order, and its place comes off sale. Nothing here moves money: no charge is made and no refund is sent.
Only a ticket canceled with the money kept can be put back. A ticket whose money went back — refunded in full, or canceled with a part refund — answers 409 not_reinstatable, because nothing can take a refund back. So do a ticket that still works, one on an order that was canceled whole (its tickets come back from the order instead), one issued by an invitation or a portal, one leg of a bundle, and one Tito has no record of canceling.
Capacity is reported rather than refused: if the place has been sold since, the call still goes through and oversold says how far past the cap the ticket type now is. No request body.
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.
Response fields
oversoldinteger- How far past its cap the ticket type now is, and 0 when the place was there. A reinstate is never refused for capacity, so this is where a caller finds out it went over.
Responses
200The ticket works again.401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.409The ticket cannot be put back (not_reinstatable).500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/tickets/H7K2PX/reinstate \
-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/reinstate", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"reinstated": true,
"ref": "H7K2PX",
"oversold": 42
}
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), on an entitlement-scoped list only tickets holding that entitlement, and on a ticket-type-limited list only tickets of those types. 0 when the list has no check-ins yet, never a missing/null value.
checkin_lists[].ticket_types_limitedboolean- True when the list checks in only the ticket types in ticket_type_ids. It combines with entitlement_id: a ticket must pass both.
checkin_lists[].ticket_type_idsarray of integer- The ticket types a limited list checks in. Always [] on a list that isn't limited. A limited list with [] here had its only types deleted and checks in nobody.
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,
"entitlement_id": 1,
"ticket_types_limited": true,
"ticket_type_ids": [
12,
14
]
}
]
}
Create a check-in list
POST /events/{slug}/checkin-lists
Create a named check-in list for the event. Set entitlement_id to limit eligible tickets to one entitlement, or omit it to include all event tickets. Set ticket_type_ids to limit it to those ticket types as well; a ticket must then pass both limits. What a list covers is set when it is created and can't be changed later.
Requires checkin.manage
Path parameters
slugstring required- The event's slug.
Request body
namestring requiredticket_type_idsarray of integer or null- The ticket types this list checks in, all of this event's (archived ones included). Leave it out, or send null, for every ticket type. An empty array is refused with 422 invalid_ticket_type_ids, and a type from another event with 404.
Response fields
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), on an entitlement-scoped list only tickets holding that entitlement, and on a ticket-type-limited list only tickets of those types. 0 when the list has no check-ins yet, never a missing/null value.
ticket_types_limitedboolean- True when the list checks in only the ticket types in ticket_type_ids. It combines with entitlement_id: a ticket must pass both.
ticket_type_idsarray of integer- The ticket types a limited list checks in. Always [] on a list that isn't limited. A limited list with [] here had its only types deleted and checks in nobody.
Responses
201Create a check-in list401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid check-in request500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/checkin-lists \
-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/events/annual-conf/checkin-lists", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Ada Lovelace"
}),
});
const data = await res.json();
{
"id": 1,
"name": "Main entrance",
"created_at": "2026-09-01T08:00:00Z",
"checked_in_count": 412,
"entitlement_id": 1,
"ticket_types_limited": true,
"ticket_type_ids": [
12,
14
]
}
Delete a check-in list
DELETE /events/{slug}/checkin-lists/{id}
Delete the check-in list and its check-ins. Tickets remain unchanged.
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).
Responses
204Deleted401No 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 \
-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: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
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 Checkinticket_types_limitedboolean- True when the list checks in only the ticket types in ticket_type_ids.
ticket_type_idsarray of integer- The ticket types a limited list checks in. Always [] on a list that isn't limited. A limited list with [] here had its only types deleted and checks in nobody.
Responses
200The list's metadata plus every check-in recorded against it, filtered exactly aschecked_in_countis (valid tickets only, entitlement-scoped and ticket-type-limited 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"
}
],
"entitlement_id": 1,
"ticket_types_limited": true,
"ticket_type_ids": [
1
]
}
Rename a check-in list
PATCH /events/{slug}/checkin-lists/{id}
Rename an event check-in list.
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
namestring
Response fields
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), on an entitlement-scoped list only tickets holding that entitlement, and on a ticket-type-limited list only tickets of those types. 0 when the list has no check-ins yet, never a missing/null value.
ticket_types_limitedboolean- True when the list checks in only the ticket types in ticket_type_ids. It combines with entitlement_id: a ticket must pass both.
ticket_type_idsarray of integer- The ticket types a limited list checks in. Always [] on a list that isn't limited. A limited list with [] here had its only types deleted and checks in nobody.
Responses
200Rename a check-in list401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid check-in request500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/checkin-lists/1 \
-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/events/annual-conf/checkin-lists/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Ada Lovelace"
}),
});
const data = await res.json();
{
"id": 1,
"name": "Main entrance",
"created_at": "2026-09-01T08:00:00Z",
"checked_in_count": 412,
"entitlement_id": 1,
"ticket_types_limited": true,
"ticket_type_ids": [
12,
14
]
}
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 and ticket-type-limited 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, ineligible for an entitlement-scoped list, or of a ticket type the list does not check in) 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
}
Duplicate a check-in list
POST /events/{slug}/checkin-lists/{id}/duplicate
Duplicate an event check-in list with the same eligibility rules and no check-ins.
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).
Response fields
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), on an entitlement-scoped list only tickets holding that entitlement, and on a ticket-type-limited list only tickets of those types. 0 when the list has no check-ins yet, never a missing/null value.
ticket_types_limitedboolean- True when the list checks in only the ticket types in ticket_type_ids. It combines with entitlement_id: a ticket must pass both.
ticket_type_idsarray of integer- The ticket types a limited list checks in. Always [] on a list that isn't limited. A limited list with [] here had its only types deleted and checks in nobody.
Responses
201Duplicate a check-in list401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/checkin-lists/1/duplicate \
-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/duplicate", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"id": 1,
"name": "Main entrance",
"created_at": "2026-09-01T08:00:00Z",
"checked_in_count": 412,
"entitlement_id": 1,
"ticket_types_limited": true,
"ticket_type_ids": [
12,
14
]
}
Get group check-in
GET /events/{slug}/checkin-lists/{id}/groups/{ref}
Return an order’s check-in breakdown by ticket type.
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).
refstring required- The order's ref (account-wide, not scoped to an event).
Response fields
order_refstringrowsarray of objectrows[].ticket_type_idintegerrows[].ticket_type_namestringrows[].totalintegerrows[].checked_inintegerrows[].remaininginteger
Responses
200Get group check-in401No 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/groups/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/groups/H7K2PX", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"order_ref": "H7K2PX",
"checked_in": 1,
"rows": [
{
"ticket_type_id": 1,
"ticket_type_name": "T9QW4M",
"total": 42,
"checked_in": 1,
"remaining": 1
}
]
}
Check in a group
POST /events/{slug}/checkin-lists/{id}/groups/{ref}
Check in eligible tickets oldest first, up to the requested count or the number remaining. New check-ins run the same workflows and record the same scans as individual check-ins.
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).
refstring required- The order's ref (account-wide, not scoped to an event).
Request body
countsobject required
Response fields
order_refstringrowsarray of objectrows[].ticket_type_idintegerrows[].ticket_type_namestringrows[].totalintegerrows[].checked_inintegerrows[].remaininginteger
Responses
200Check in a group401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid check-in request500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/checkin-lists/1/groups/H7K2PX \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"counts":null}'
// 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/groups/H7K2PX", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"counts": null
}),
});
const data = await res.json();
{
"order_ref": "H7K2PX",
"checked_in": 1,
"rows": [
{
"ticket_type_id": 1,
"ticket_type_name": "T9QW4M",
"total": 42,
"checked_in": 1,
"remaining": 1
}
]
}
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.
After attendees move between events, people count at the event their ticket is at and every penny of an order counts at the event it was paid at. moved_out names the part of collected_cents that is for people now at other events, and moved_in what people who moved here paid elsewhere. GET /events/{slug}/reports/moved lists them. Needs events.view as well as financials.view.
Requires financials.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.
moved_out_centsinteger- The part of
collected_centsthat is for people who have since moved to another event and still hold their ticket. Nothing is taken off: an order's money stays at the event it was paid at, and the person counts at the event they moved to. Zero when nobody moved away. moved_outarray of objectmoved_out_centsby the event those people moved to. Empty when nobody moved away.moved_out[].peopleinteger- How many people moved between the two events and still hold their ticket.
moved_out[].paid_centsinteger- What those people paid, on their orders at the event the money stays at.
moved_inarray of object- People who moved HERE from another event, by that event, and what they paid there. None of it is in this report's figures, which is why it is listed: it is in the income of the event named. Read whatever the range, because none of it is in this ledger to narrow. Empty when nobody moved here.
moved_in[].peopleinteger- How many people moved between the two events and still hold their ticket.
moved_in[].paid_centsinteger- What those people paid, on their orders at the event the money stays at.
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,
"moved_out_cents": 14900,
"moved_out": [
{
"event": "example",
"event_name": "Ada Lovelace",
"people": 1,
"paid_cents": 14900
}
],
"moved_in": [
{
"event": "example",
"event_name": "Ada Lovelace",
"people": 1,
"paid_cents": 14900
}
],
"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, discount code or link tag. 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. Needs events.view as well as financials.view.
Requires financials.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), ortag:<name>for the values of one link tag the orders arrived with, liketag:utm_source. Orders that arrived without that tag share one row with an emptykey. A tag no order of this event carries isday, and so is anything else. Narrow to one value of a tag with a parameter of the same name,tag:utm_source=newsletter, or-for the orders without it. The tags that ran come back astags. 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. tagsobject- The link tag filters that ran, tag name to value,
-meaning orders without that tag. Empty when none ran. 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",
"tags": {},
"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 the event’s saved reports. Reports using sources the key cannot read are omitted.
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.404No such resource.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",
"share": null
}
]
}
Create a saved report
POST /events/{slug}/reports
Create a saved report from a JSON name, optional source, and query URL query string. The source defaults to orders and requires its read permission; blank names are rejected and names over 80 characters are trimmed as in the UI.
Requires events.manage
Path parameters
slugstring required- The event's slug.
Request body
Response fields
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.
Responses
201Saved report401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid report definition500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/reports \
-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/events/annual-conf/reports", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Ada Lovelace"
}),
});
const data = await res.json();
{
"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",
"share": null
}
Moved between events
GET /events/{slug}/reports/moved
Who moved between this event and another, when, and what they paid: the admin's Moved between events report. People count at the event their ticket is at, and every penny of an order counts at the event it was paid at, so moved_out is people whose money stays in this event's reports and moved_in is people whose money is in the reports of event. A ticket canceled since is listed with valid false. Needs events.view as well as financials.view.
Requires financials.view
Path parameters
slugstring required- The event's slug.
Query parameters
withstring- Another event's slug. Narrows both lists to moves between this event and that one.
Response fields
moved_out[].ticket_typestring- In
moved_out, the type they held at this event. Inmoved_in, the type they hold here now. moved_out[].eventstring- The other event's slug: where they went, or where they came from (which holds their order and its money).
moved_out[].paid_centsinteger- What the ticket brought in, any difference the move charged or refunded included.
moved_in[].ticket_typestring- In
moved_out, the type they held at this event. Inmoved_in, the type they hold here now. moved_in[].eventstring- The other event's slug: where they went, or where they came from (which holds their order and its money).
moved_in[].paid_centsinteger- What the ticket brought in, any difference the move charged or refunded included.
Responses
200Operation completed.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/reports/moved \
-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/moved", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"moved_out": [
{
"ticket": "T9QW4M",
"name": "Ada Lovelace",
"ticket_type": "T9QW4M",
"event": "example",
"event_name": "Ada Lovelace",
"moved_at": "2026-10-14T09:00:00Z",
"valid": true,
"paid_cents": 14900,
"currency": "eur"
}
],
"moved_in": [
{
"ticket": "T9QW4M",
"name": "Ada Lovelace",
"ticket_type": "T9QW4M",
"event": "example",
"event_name": "Ada Lovelace",
"moved_at": "2026-10-14T09:00:00Z",
"valid": true,
"paid_cents": 14900,
"currency": "eur"
}
]
}
Delete a saved report
DELETE /events/{slug}/reports/{id}
Delete a saved report. The request requires permission to read its source.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Responses
204Deleted401No 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/reports/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/reports/1", {
method: "DELETE",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
Get a saved report
GET /events/{slug}/reports/{id}
Return one saved report. The request requires permission to read its source.
Requires events.view
Path parameters
slugstring required- The event's slug.
idinteger required
Response fields
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.
Responses
200Saved report401No 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/reports/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/reports/1", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"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",
"share": null
}
Update a saved report
PATCH /events/{slug}/reports/{id}
Change the report name, query, or both. The query uses the same defaults and validation as the UI and removes drill and page; the report’s source cannot change.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Request body
Response fields
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.
Responses
200Saved report401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Invalid report definition500Internal error.
curl -X PATCH https://<yourslug>.go.tito.io/api/v1/events/annual-conf/reports/1 \
-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/events/annual-conf/reports/1", {
method: "PATCH",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Ada Lovelace"
}),
});
const data = await res.json();
{
"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",
"share": null
}
Duplicate a saved report
POST /events/{slug}/reports/{id}/duplicate
Duplicate a saved report with its definition and source. The request requires permission to read its source.
Requires events.manage
Path parameters
slugstring required- The event's slug.
idinteger required
Response fields
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.
Responses
201Saved report401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/events/annual-conf/reports/1/duplicate \
-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/1/duplicate", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"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",
"share": null
}
Run a saved report
GET /events/{slug}/reports/{id}/results
Run a saved report through the UI report engine. Requires events.view and the source’s read permission. The response includes the definition, effective query, source, timezone, currency, columns, rows, and totals. Money uses integer currency minor units. Percentages are whole percentage points rounded as in the UI. Columns include the measures shown in the UI or CSV. Record drill-through and binary exports are not included.
Requires events.view
Path parameters
slugstring required- The event's slug.
idinteger required
Query parameters
currencystring- Read the report in an enabled event currency without changing its saved definition. Omit to use the saved choice or event currency.
Response fields
reportobjectreport.idinteger- The report's row id; saved reports carry no ref (admin-only, never a public surface).
report.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).
report.created_atstring (RFC 3339)- Integer Unix timestamp in storage, rendered as RFC 3339 UTC. Not nullable (NOT NULL column).
report.updated_atstring (RFC 3339)- When the name or the definition last changed. Equals
created_aton a report nobody has edited. querystringtimezonestringcurrencystringcolumnsarray of objectcolumns[].labelstringrowsarray of ReportValuesrows[].keystringrows[].labelstringrows[].valuesobject- Integer measures keyed by the column key. Money is in currency minor units; percentages are rounded whole percentage points.
totalsobjecttotals.keystringtotals.labelstringtotals.valuesobject- Integer measures keyed by the column key. Money is in currency minor units; percentages are rounded whole percentage points.
Responses
200The saved report results. Empty reports have an empty rows array and zero totals.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/reports/1/results \
-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/1/results", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"report": {
"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",
"share": null
},
"query": "example",
"source": "orders",
"timezone": "Europe/Dublin",
"currency": "eur",
"columns": [
{
"key": "example",
"label": "example",
"kind": "count"
}
],
"rows": [
{
"key": "example",
"label": "example",
"values": {}
}
],
"totals": {
"key": "example",
"label": "example",
"values": {}
}
}
Widgets
The embeddable ticket lists an organizer pastes into their own site, and the domains they have been seen on.
Interest
GET /events/{slug}/interest
The people who asked to hear about this event through one of its widgets: their email, their name where the list has one, the consent they gave, and when they confirmed it.
Requires contacts.view
Path parameters
slugstring required- The event's slug.
Response fields
interestarray of objectinterest[].idintegerinterest[].widget_idintegerinterest[].emailstringinterest[].namestring- The person's name, or empty. A widget asks for an email only, so a name is there only for somebody whose sign-up moved from Tito Classic.
interest[].consentstringinterest[].created_atintegerinterest[].confirmed_atinteger
Responses
200Interest401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.
curl https://<yourslug>.go.tito.io/api/v1/events/annual-conf/interest \
-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/interest", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"interest": [
{
"id": 1,
"widget_id": 1,
"email": "ada@example.com",
"name": "Ada Lovelace",
"consent": "example",
"created_at": 1,
"confirmed_at": 1
}
]
}
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 types it lists, in the order it lists them. EMPTY IS EVERYTHING ON SALE, including types made later — not "lists nothing".
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)widgets[].analyticsboolean- Hands the widget's own events to the tracking already on the organizer's page. Off unless asked for.
widgets[].all_ticket_types_linkboolean- When the widget lists only some ticket types, its list ends with an "All ticket types" link that reopens it showing every ticket type on sale, keeping the quantities already picked. Off unless asked for.
widgets[].full_page_linkboolean- Whether the widget's checkout shows an "Open the full page" link in its footer, which carries the buyer to the same step on the event's own Tito page, holding the same order. On unless switched off.
widgets[].show_promo_fieldboolean- Whether the widget shows the promotion code field, on its ticket list, its order review and the booking form reached from it. Only an event with codes that can be used shows it at all. On unless switched off. The event page's own field is set in Customize.
widgets[].prefill_reviewboolean- When the website the widget sits on hands over a buyer's details with tito("widget.create", …), whether the widget shows the filled-in form first. Off, the buyer goes straight on to paying once nothing the event asks is missing. Off unless switched on.
widgets[].openingstring- How the widget opens over the organizer's page. Always
push: the page pushes back as one rounded card and the tickets rise over it. Visitors who ask their device for less motion get a fade. - one of
push widgets[].interest_messagestringwidgets[].legacy_tokenstring- The Tito Pro widget token this widget also answers to, so a website moving from Tito Pro keeps the
data-tito-tokenandtito("widget.open", …)it already has. Empty for a widget made in Tito Go. Carried over by the account import and never set through this API. An address the website already prints, not a credential. widgets[].bannerstring- What the widget shows across the top of its ticket list:
""for no banner,"event"for the event's own banner (followed as it changes), an https image URL, or"asset:<id>", a picture uploaded on the widget's page in the admin. widgets[].show_event_nameboolean- Shows the event's name as a heading above the ticket list. Off by default.
widgets[].show_dateboolean- Shows the event's date and time above the ticket list, in the event's own time zone. Off by default.
widgets[].show_venueboolean- Shows the venue's name and address above the ticket list, the address opening a map in a new tab. Off by default.
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",
"analytics": false,
"share_buyer": false,
"all_ticket_types_link": false,
"full_page_link": true,
"show_promo_field": true,
"prefill_review": false,
"opening": "push",
"purpose": "purchase",
"interest_message": "example",
"legacy_token": "",
"banner": "event",
"show_event_name": false,
"show_date": false,
"show_venue": false,
"show_descriptions": true
}
]
}
List widget domains
GET /widget-domains
List the websites the account's widgets have been seen on or that somebody added, 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[].sightingsinteger- How many times the widget has loaded there. 0 on a website added with
POST /widget-domainsuntil it first does. widget_domains[].first_seen_atstring (RFC 3339) or null- When the widget first loaded there; null on a website added by hand that it hasn't loaded on yet.
widget_domains[].last_seen_atstring (RFC 3339) or null- When the widget last loaded there; null on a website added by hand that it hasn't loaded on yet.
widget_domains[].decided_atstring (RFC 3339) or null- When somebody answered about this website; null while nobody has.
Responses
200Account-level, not event-scoped: a domain is a statement of trust about a website, not a property of one ticket list. A row arrives by being SEEN (the widget loaded there, and it startspending) or by being added ahead of time (POST /widget-domains, the organizer's Add a website, which startsapprovedwithsightings0 andfirst_seen_atandlast_seen_atnull until the widget first loads there). 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",
"decided_at": "2026-08-27T18:20:00Z"
}
]
}
Webhooks
Outbound webhook endpoints, their delivery log, and the writes that make, change, test and delete them.
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 not returned here. It is read or replaced only through the explicit secret routes, whose bodies stay out of the API log. Endpoints are made, changed (their event types and their on/off switch), tested, replayed and deleted through the write routes.
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.completed, order.updated, 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.updated, ticket.moved, ticket.voided, ticket.unvoided, ticket.checked_in, checkin.created, checkin.deleted, interest.created, interest.deleted. order.updated fires when the buyer's name, email, company or phone, or an order-level answer, changes. ticket.moved fires when a ticket moves to another event: its event block is the event it is now on, moved_from is {"event": {"slug": ..., "name": ...}, "ticket_type": "..."} naming where it came from, and its order block carries event ({"slug", "name"}), the order's own event, which may differ. ticket.unvoided fires when a voided ticket is brought back. interest.created fires when somebody confirms an interest-list sign-up and interest.deleted when they leave the list; their data is {"event": {"slug", "name"}, "account": {"slug", "name"}, "interest": {"id", "email", "name", "locale", "status", "created_at", "confirmed_at", "withdrawn_at"}}. checkin.deleted, interest.created and interest.deleted are webhook-only: no workflow trigger carries them. An endpoint made before a type existed does not get it until its event types are changed to include it. order.created (shown to organizers as Order started) fires when an order is made, with one exception: a checkout on the booking form whose buyer was not told they may be emailed about the event fires it only when the order completes, and never if they walk away, in which case no order.cancelled follows either. order.completed fires once per order, when it is paid by any means, placed by bank transfer, or made by the organizer (in the admin, through this API, or by converting a quote); when it lands with order.created or order.paid it comes after the first and before the second. 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 delivers checkin.deleted, the same data with checkin.deleted_at (integer Unix timestamp) added; 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. A recorded delivery can be sent again through the redeliver route.
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"
}
]
}
Replace a webhook signing secret
POST /webhooks/{id}/rotate-secret
Explicit signing-secret access. Successful responses have Cache-Control: no-store. Request and response bodies are omitted from the API log. Standard webhook response shapes still omit the secret. The old secret stops working immediately. Update the receiver. This endpoint has no idempotency-key support. After an uncertain response, read the current secret instead of blindly rotating again.
Requires settings.manage
Path parameters
idinteger required
Request body
Response fields
secretstring
Responses
200Current signing secret401No key, an unknown key, or a revoked key.403The key does not carry the required capability.404No such resource.422Confirmation is required500Internal error.
curl -X POST https://<yourslug>.go.tito.io/api/v1/webhooks/1/rotate-secret \
-H "Authorization: Bearer titogo_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"confirmed":true}'
// Node 18+, Deno, Bun, or a browser on the same origin
const res = await fetch("https://<yourslug>.go.tito.io/api/v1/webhooks/1/rotate-secret", {
method: "POST",
headers: {
Authorization: "Bearer titogo_sk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
"confirmed": true
}),
});
const data = await res.json();
{
"secret": "example"
}
Get a webhook signing secret
GET /webhooks/{id}/secret
Explicit signing-secret access. Successful responses have Cache-Control: no-store. Request and response bodies are omitted from the API log. Standard webhook response shapes still omit the secret.
Requires settings.manage
Path parameters
idinteger required
Response fields
secretstring
Responses
200Current signing secret401No 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/secret \
-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/secret", {
method: "GET",
headers: {
Authorization: "Bearer titogo_sk_your_key",
},
});
const data = await res.json();
{
"secret": "example"
}
Workflows
Automations that run when something happens in the account, and the runs they produced.
List workflows
GET /workflows
Every workflow on the account, without what each one does. Read one to see its definition.
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
Downloads the account as a SQLite snapshot. Requires export.pii. The snapshot removes webhook signing secrets, terminal-reader credentials, API-key hashes, preview capabilities, staff review links, and stored API request and response bodies. Other request metadata remains. Buyer order and ticket capability tokens are included as account data. The live account is unchanged. Reissuing the removed credentials is required before using this snapshot as a live database.
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 OutboxEmail object
Attributes
idintegermessage_idintegerrecipientstringsubjectstringstatusstringdeliverystring- What the email provider said became of a sent email. Empty when there is nothing to report: not sent yet, or sent through a mail transport that can't report back (SMTP, or a sender imported from Tito Classic or Pro), and
statussays what it is. - one of
deliveredwaitingbouncednot_sentmarked_as_spam delivery_reasonstring- The provider's classification of a bounce, a delay or a spam report, such as
NoEmailorMailboxFull. Empty when it gave none. fixed_tostring- The address this email's recipient was corrected to on their ticket or order, when it hasn't been sent there yet.
recipientstays the address it was sent to. Empty otherwise. created_atinteger
{
"id": 1,
"message_id": 1,
"recipient": "example",
"subject": "example",
"status": "ticket",
"delivery": "",
"delivery_reason": "example",
"fixed_to": "example",
"created_at": 1
}
The OutboxEmailDetail object
Attributes
idintegermessage_idintegerrecipientstringsubjectstringstatusstringdeliverystring- What the email provider said became of a sent email. Empty when there is nothing to report: not sent yet, or sent through a mail transport that can't report back (SMTP, or a sender imported from Tito Classic or Pro), and
statussays what it is. - one of
deliveredwaitingbouncednot_sentmarked_as_spam delivery_reasonstring- The provider's classification of a bounce, a delay or a spam report, such as
NoEmailorMailboxFull. Empty when it gave none. fixed_tostring- The address this email's recipient was corrected to on their ticket or order, when it hasn't been sent there yet.
recipientstays the address it was sent to. Empty otherwise. created_atintegerbodystringhtmlstringsenderstring
{
"id": 1,
"message_id": 1,
"recipient": "example",
"subject": "example",
"status": "ticket",
"delivery": "",
"delivery_reason": "example",
"fixed_to": "example",
"created_at": 1,
"body": "example",
"html": "example",
"sender": "example"
}
The Error object
The one error shape returned by every route.
Attributes
errorobjecterror.codestring- Machine-readable error code, e.g. "not_found", "missing_key", "unknown_key", "missing_capability", "sandbox_read_only", "internal". "sandbox_read_only" is answered inside an account sandbox by the routes that configure the account rather than act inside it: a sandbox is set up from the live account, so those writes belong there. "test_mode_from_live" is answered in test mode by a write to a portal or an invitation list that came from the live account, which can only be changed there. One made in test mode can be changed, and stays in test mode.
error.messagestring- Human-readable, deliberately English (machine/API error strings are outside the i18n scope).
{
"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 RemovedOrderNote object
The one row that survives Remove test order: enough to explain a payment or refund at Stripe or PayPal that carries the order's ref.
Attributes
refstringamount_centsintegercurrencystringpayment_providerstringstripe,paypalorsimulated; null for an order nobody paid for through a provider.payment_refstring- The provider's reference for the payment: a Stripe payment intent, a PayPal capture, or the simulated payment's own reference.
refund_refstring- The provider's reference for the last refund made against the order; null when there was none.
removed_atstring (RFC 3339)moved_tostring- Where the order went instead of being deleted outright:
sandboxwhen it was copied into the account's sandbox first; null for a plain delete, which is every removal this endpoint makes. moved_refstring- The ref the copy carries wherever it went, which is minted from its own row and can differ from
ref. Null when nothing was moved. invoicesarray of string- The numbers of the invoices issued for the order, oldest first. An invoice keeps its number when its order is removed, and this is where that number leads back to. Empty when none was issued.
{
"ref": "H7K2PX",
"status": "ticket",
"amount_cents": 14900,
"currency": "eur",
"tickets": 1,
"payment_provider": "example",
"payment_ref": "H7K2PX",
"refund_ref": "H7K2PX",
"removed_at": "2026-10-14T09:00:00Z",
"removed_by": "example",
"moved_to": "example",
"moved_ref": "H7K2PX",
"invoices": [
"example"
]
}
The RemoveResult object
What the remove endpoint answers on a 200 — always a fresh removal; an order that cannot be removed is an error response, never a false here.
Attributes
noteobject- The one row that survives Remove test order: enough to explain a payment or refund at Stripe or PayPal that carries the order's ref.
note.refstringnote.amount_centsintegernote.currencystringnote.payment_providerstringstripe,paypalorsimulated; null for an order nobody paid for through a provider.note.payment_refstring- The provider's reference for the payment: a Stripe payment intent, a PayPal capture, or the simulated payment's own reference.
note.refund_refstring- The provider's reference for the last refund made against the order; null when there was none.
note.removed_atstring (RFC 3339)note.moved_tostring- Where the order went instead of being deleted outright:
sandboxwhen it was copied into the account's sandbox first; null for a plain delete, which is every removal this endpoint makes. note.moved_refstring- The ref the copy carries wherever it went, which is minted from its own row and can differ from
ref. Null when nothing was moved. note.invoicesarray of string- The numbers of the invoices issued for the order, oldest first. An invoice keeps its number when its order is removed, and this is where that number leads back to. Empty when none was issued.
{
"removed": true,
"ref": "H7K2PX",
"note": {
"ref": "H7K2PX",
"status": "ticket",
"amount_cents": 14900,
"currency": "eur",
"tickets": 1,
"payment_provider": "example",
"payment_ref": "H7K2PX",
"refund_ref": "H7K2PX",
"removed_at": "2026-10-14T09:00:00Z",
"removed_by": "example",
"moved_to": "example",
"moved_ref": "H7K2PX",
"invoices": [
"example"
]
}
}
The RemovedOrderError object
The 410 a removed test order's ref answers: the standard error envelope with code removed, plus the note.
Attributes
errorobjecterror.messagestringnoteobject- The one row that survives Remove test order: enough to explain a payment or refund at Stripe or PayPal that carries the order's ref.
note.refstringnote.amount_centsintegernote.currencystringnote.payment_providerstringstripe,paypalorsimulated; null for an order nobody paid for through a provider.note.payment_refstring- The provider's reference for the payment: a Stripe payment intent, a PayPal capture, or the simulated payment's own reference.
note.refund_refstring- The provider's reference for the last refund made against the order; null when there was none.
note.removed_atstring (RFC 3339)note.moved_tostring- Where the order went instead of being deleted outright:
sandboxwhen it was copied into the account's sandbox first; null for a plain delete, which is every removal this endpoint makes. note.moved_refstring- The ref the copy carries wherever it went, which is minted from its own row and can differ from
ref. Null when nothing was moved. note.invoicesarray of string- The numbers of the invoices issued for the order, oldest first. An invoice keeps its number when its order is removed, and this is where that number leads back to. Empty when none was issued.
{
"error": {
"code": "removed",
"message": "example"
},
"note": {
"ref": "H7K2PX",
"status": "ticket",
"amount_cents": 14900,
"currency": "eur",
"tickets": 1,
"payment_provider": "example",
"payment_ref": "H7K2PX",
"refund_ref": "H7K2PX",
"removed_at": "2026-10-14T09:00:00Z",
"removed_by": "example",
"moved_to": "example",
"moved_ref": "H7K2PX",
"invoices": [
"example"
]
}
}
The AnonymizeResult object
What an anonymize call answers on a 200: what went, what stayed, and what Tito could not reach, the admin confirm step's three lists as counts.
Attributes
refstring- The record's reference: a ticket, order, customer, waitlist entry or ticket request ref, or the invitation's id.
anonymized_atstring (RFC 3339)removedobjectremoved.emailsinteger- Sent emails whose address and words were removed. The record that each was sent stays.
removed.link_tagsinteger- Orders whose link tags were removed: every tag that isn't a
utm_*campaign tag, ad click ids included. removed.interestboolean- Their email address was taken off the mailing list, where they signed up to hear when tickets go on sale.
removed.cardboolean- A card saved with a ticket request this withdrew, which Stripe was asked to delete.
keptobjectkept.other_recordsinteger- Other orders, tickets and invitations with the same email address, which are not changed.
kept.customerstring or null- The customer record an anonymized order is linked to, which is not changed.
kept.campaign_tagsinteger- Orders whose
utm_*campaign tags were kept, so they still count toward the link that sold them. kept.orderstring or null- The order a waitlist place was taken with, or that a ticket request's invitation was claimed with, which is not changed.
kept.invitationinteger or null- The invitation a ticket request was approved into, which is not changed.
outside_titoobjectoutside_tito.payment_providerstring or null- The provider that keeps its own record of the payment, with the buyer's name and email address.
- one of
StripePayPal<nil> outside_tito.servicesarray of string- Connected services the person's details were sent to, which keep their own copy.
outside_tito.cardboolean- A card saved with a ticket request that was already decided, which Stripe keeps.
{
"anonymized": true,
"kind": "ticket",
"ref": "H7K2PX",
"anonymized_at": "2026-10-14T09:00:00Z",
"removed": {
"tickets": 1,
"orders": 1,
"quotes": 1,
"answers": 1,
"files": 1,
"emails": 1,
"contact": true,
"waitlist": true,
"ticket_request": true,
"link_tags": 1,
"interest": true,
"card": true
},
"kept": {
"documents": 1,
"other_records": 1,
"customer": "example",
"campaign_tags": 1,
"order": "H7K2PX",
"ticket": "T9QW4M",
"invitation": 1
},
"outside_tito": {
"payment_provider": "Stripe",
"services": [
"example"
],
"webhooks": true,
"card": true
}
}
The DataProtectionLogEntry object
One line of the data-protection log.
Attributes
idintegeractionstring- one of
ticket.anonymizedorder.anonymizedinvitation.anonymizedbuyer.anonymizedwaitlist.anonymizedapplication.anonymizedevent.deletedevent.delete_requestedevent.delete_keptevent.anonymize_requestedevent.anonymize_keptevent.anonymizedaccount.close_requestedaccount.close_kept subject_refstring- The record's reference, or the event's id, or empty for the account itself. Never the person.
eventstring or null- The slug of the event the record belongs to, null for a customer or a deleted event.
atstring (RFC 3339)
{
"id": 1,
"action": "ticket.anonymized",
"subject_type": "ticket",
"subject_ref": "H7K2PX",
"event": "example",
"actor": "example",
"at": "2026-10-14T09:00:00Z"
}
The Unsubscribe object
One person's unsubscribe, from one event's messages or from the whole account's.
Attributes
idintegerscopestringeventstops one event's messages;accountstops every message the account sends.- one of
eventaccount reasonstringunsubscribedis the person's own choice, and the only reason recorded today.- one of
unsubscribedbouncedcomplained message_idinteger or null- The id of the message whose link they used, one of the event's messages; null when there was none.
created_atstring (RFC 3339)
{
"id": 1,
"email": "ada@example.com",
"scope": "event",
"event": {
"slug": "annual-conf",
"name": "Ada Lovelace"
},
"reason": "unsubscribed",
"message_id": 1,
"created_at": "2026-10-14T09:00:00Z"
}
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 OrderInvoiceDetailsRequest object
Attributes
buyer_referencestring- A code the buyer's organization uses to route invoices, such as Germany's Leitweg-ID.
{
"bill_to": "example",
"vat_number": "example",
"address_line1": "example",
"address_line2": "example",
"city": "example",
"postcode": "EARLYBIRD",
"subdivision": "example",
"country": "example",
"buyer_reference": "example",
"po_reference": "example",
"email_buyer": true
}
The OrderDueDateRequest object
Attributes
{
"due_date": "2026-10-14T09:00:00Z"
}
The OrderDueDateResult object
The invoice's new due date.
Attributes
{
"ref": "H7K2PX",
"due_date": "2026-10-14T09:00:00Z",
"due_at": "2026-10-14T09:00:00Z"
}
The OrderReinstateRequest object
The one send a reinstate can make. Send no body at all to answer no.
Attributes
email_buyerboolean- Email the buyer to say their order is back on and their tickets work again. Off unless asked for.
{
"email_buyer": true
}
The ReinstateResult object
What the reinstate endpoint answers on a 200. An order that was not canceled is an error response, never a false here.
Attributes
oversoldinteger- How far past its cap the ticket type now is, and 0 when the places were there. A reinstate is never refused for capacity, so this is where a caller finds out it went over.
{
"reinstated": true,
"ref": "H7K2PX",
"tickets": 1,
"oversold": 42
}
The TicketsMoveResult object
What the move tickets endpoint answers on a 200.
Attributes
{
"to_event": "example",
"moved": [
"example"
],
"staying": [
{
"ref": "H7K2PX",
"reason": "void"
}
],
"unknown": [
"example"
],
"failed": 1
}
The TicketCancelResult object
What the ticket cancel endpoint answers on a 200.
Attributes
{
"canceled": true,
"ref": "H7K2PX",
"already": true
}
The TicketReinstateResult object
What the ticket reinstate endpoint answers on a 200. A ticket that was not canceled is an error response, never a false here.
Attributes
oversoldinteger- How far past its cap the ticket type now is, and 0 when the place was there. A reinstate is never refused for capacity, so this is where a caller finds out it went over.
{
"reinstated": true,
"ref": "H7K2PX",
"oversold": 42
}
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.
search_indexingboolean- Whether this account has opened its public site to search engines. False until an organizer turns it on, which is where every account starts, and while it is false nothing on the site is listed anywhere: the account's event list, its series pages and every event page all carry a noindex instruction. It closes nothing and gates nothing. An event with search_indexing "inherit" — the default — follows this value, so moving it moves every such event with it.
home_pagestring- What "/" shows on the account's own host:
events,event:<slug>orpage:<path>. An unreadable stored value answers asevents, the way the door itself reads it. root_pagesboolean- Whether the account's pages answer on the root domain as well. ABSENT on an instance with no root domain — there is nothing for it to be true or false about. Unset reads as true: the root domain's URLs are the default surface.
close_onstring (RFC 3339) or null- When the account is due to be closed, 30 days after its owner asked for it in Account settings. Null when it isn't closing. Asking and keeping the account happen in the admin only: asking emails the owner and every admin.
{
"slug": "acme",
"name": "Acme Events",
"region": "uk",
"search_indexing": false,
"home_page": "events",
"root_pages": true,
"close_on": null
}
The Event object
An event: something you sell tickets to. Addressed everywhere by its slug.
Attributes
admin_urlstring- Links to the event admin page, as an absolute URL: on the root domain, or on the instance's own address on a standalone account.
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 map the event page shows first; people can switch it themselves, and their browser remembers what they picked.
autois Apple Maps on Apple devices, OpenStreetMap on Linux and Google Maps elsewhere. A stored preference, not a promise about what people see: a service the page cannot draw ("apple" without Apple credentials, "osm" without a map pin) falls back to one it can, and an event with no map at all shows the address and its open-in links. - one of
autoapplegoogleosm ends_atstring (RFC 3339) or null- When the event ends, in the same form as
starts_at. Null when the event has no end time set. all_dayboolean- Whether the event's dates are shown without times. A date-only
starts_atsent to a create or a PATCH sets it. date_textstring- Free text the event page shows instead of the dates ("June 2027", "TBC"). Empty when the dates are shown.
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".
unlistedboolean- True when the event is unlisted: anyone with the link. The event is absent from the account's public event list and from its series page, and nothing else changes — its main page answers everybody, it sells, and it is indexable unless search_indexing says otherwise. A listing leaves it out; a link to it keeps working. Independent of secret; where both are true, secret is the stronger answer.
archivedboolean- True when the event has been archived (
POST /events/{slug}/archive). It is off the admin's everyday lists and the account's public event list and series pages, and its main page answers 404 to anyone not signed in, as a secret event's does. Orders, tickets and every link already sent keep working. A listing leaves it out. delete_onstring (RFC 3339) or null- When the event will be deleted, after the account's owner asked for it to be deleted in the admin. Null while it is not waiting to be deleted. An event with orders, tickets, invitations or ticket requests waits 30 days, and until then registration is closed: it is off the public lists, its main page answers 404 to anyone not signed in, and nothing can be bought or claimed.
POST /events/{slug}/keeptakes the request back and reopens registration. anonymize_onstring (RFC 3339) or null- When the attendee data on the event will be anonymized, after the account's owner asked for it in the admin. Null while it is not waiting to be. It waits 30 days and registration stays open meanwhile, so anyone who registers before then is anonymized too. On the day every buyer's, attendee's and invitee's details are removed, and the orders, what they cost and when they were paid stay.
POST /events/{slug}/keep-datatakes the request back until then. search_indexingstring- This event's OWN answer to whether search engines may list it. "inherit" means the account's search_indexing switch decides, which is where every event starts and most stay. "on" and "off" override it for this event only. Read no_index for the resolved answer.
- one of
inheritonoff no_indexboolean- The RESOLVED answer, and the one to read: true when this event is asking search engines not to list it, whether that came from its own switch or from the account's. It closes nothing and gates nothing — the page answers exactly as it did, to exactly the same people, the event stays on whatever lists it was already on, and every link still sells — so do not read it as a visibility gate the way you read draft. It is independent of secret in both directions. Resolved rather than raw because the raw field alone gets the common case backwards: the account's search_indexing is off until somebody turns it on, so an event that has never touched its own switch is out of search while its own setting still says "inherit".
passed_params_onboolean- Whether the query params on a buyer's link are kept on the order they place. On by default. This is the switch and it is the only way off: writing an empty
passed_params_patternresets to the default rather than stopping capture. passed_params_patternstring- Which param names are kept, as comma-separated globs anchored at both ends.
utm_*keepsutm_source,utm_mediumand the rest. Click ids such asgclidandfbclidare not in the default and are added by name, because a click id identifies a person where a campaign name identifies a campaign. calendar_on_event_pageboolean- Whether the event page offers Add to calendar beside its date, before anybody books: Google Calendar, Outlook and a calendar file of the event's own date, carrying only what the page shows. Off until the organizer turns it on. Separate from
calendar_links_enabledonGET /events/{slug}/settings/after-registering, which is the offer after booking, on the order page, the ticket and the emails. secret_redirect_urlstring- Where the event's page sends visitors while the event is
secret, as a 302 on the account's own addresses. Empty when there is none, and then a secret event's page is not found. Kept while the event is public, and only used while it is secret. Calendar entries link it in place of the event page while it is used. 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.
{
"admin_url": "https://go.tito.io/youraccount/annual-conf/admin",
"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",
"ends_at": "2026-10-15T17:00:00Z",
"all_day": false,
"date_text": "",
"timezone": "Europe/Dublin",
"currency": "eur",
"currencies": [
"eur",
"gbp"
],
"currency_assignment": "switcher",
"created_at": "2026-03-02T11:20:41Z",
"updated_at": "2026-03-09T16:04:12Z",
"draft": false,
"secret": false,
"unlisted": false,
"archived": false,
"archived_at": "2026-03-09T16:04:12Z",
"delete_on": "2026-11-10T14:30:00Z",
"anonymize_on": "2026-11-10T14:30:00Z",
"search_indexing": "inherit",
"no_index": false,
"share_title": "",
"share_text": "",
"share_image_url": "/a/8f2c1d9e4b7a6350.png",
"passed_params_on": true,
"passed_params_pattern": "utm_*",
"calendar_on_event_page": false,
"secret_redirect_url": "https://example.com/tickets",
"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- Tickets issued at this rung: seats on paid orders, on unpaid orders whose tickets already exist, and added to an order afterwards. 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.
hide_freeboolean- Whether the event page and the widget leave the word Free off this ticket. Only ever applies to a free ticket: a ticket with a price always shows it.
signup_hide_nameboolean- Whether a free event's sign-up form leaves this ticket's name and price off, showing only the questions and Confirm.
pay_what_you_wantboolean- Whether buyers choose this ticket's price. When true, price_cents and prices are the least a buyer can pay, and suggested_price_cents is what the price box on the event page and the widget starts on. Surfaces with no price box (invitations, waitlist offers, the API's own checkout) charge price_cents.
suggested_price_centsinteger- The price a pay-what-you-want ticket's box starts on, in minor units of the event's currency. 0 means no suggestion, and it is always 0 when pay_what_you_want is false.
show_left_atinteger- How few of this ticket have to be left before the event page and the widget say how many. 0 is never, and is the default.
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.
descriptionstring- Organizer text shown under the ticket's name. Empty when there is none. Plain text, or a composer body (the stored form of a message body) when it carries bold, italic, links or bullets. Send either back as it is. description_text and description_html are this read for a person.
description_textstring- The description as plain lines: a bullet starts with a bullet character and a link is its words followed by its address in brackets. The same as description when nothing is formatted.
description_htmlstring- The description as HTML, the way a buyer page draws it: escaped text whose line breaks are newlines, with strong, em, links that open in a new tab, and ul lists. Escaped plain text when nothing is formatted.
tax_rate_idinteger- The tax rate this ticket charges (
GET /tax-rates), or 0 for no tax. The same name the create and PATCH bodies take. price_includes_taxboolean- Whether
price_centsalready contains the tax. Always false whentax_rate_idis 0. second_tax_rate_idinteger- A second tax rate this ticket charges beside
tax_rate_id, or 0 for none. Each rate is charged on the price, never on the other's tax (Canada's GST and QST). reverse_chargestring- Who pays no VAT on this ticket:
""for nobody,countryfor companies inreverse_charge_country, oreufor companies in EU countries other than the account's own. A company qualifies only once the EU register confirms its VAT number while it books. reverse_charge_countrystring- The country whose companies get reverse charge, as an ISO 3166 code, when
reverse_chargeiscountry. Empty otherwise. reverse_charge_notestring- The organizer's own wording for the invoice of a reverse-charged order. Empty for the standard sentence.
colorstring- The ticket's color in the admin's lists: one of
slate,red,amber,green,teal,blue,indigoorpink, or empty for none. allow_downgradeboolean- Whether this type is ticked under the event's
allow_downgrades. A buyer may change a ticket of this type down to a cheaper one, and get the difference back, when both are on. The same name PATCH takes. allow_upgradeboolean- Whether this type is ticked under the event's
allow_upgrades. A buyer may change a ticket of this type up to a dearer one, and pay the difference, when both are on. The same name PATCH takes. holder_changes_modestringinheritthe event's rule, orallow/denyholder changes on this type.- one of
inheritallowdeny holder_changes_untilstring- The deadline for holder changes when the mode is allow, as the form stores it; empty for none, and always empty while the mode is inherit.
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- Tickets issued at this rung: seats on paid orders, on unpaid orders whose tickets already exist, and added to an order afterwards. 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.ticket_type_idsarray of integer- Every companion ticket type, any of which counts. They count together: one of each covers two.
companion.per_quantityinteger- How many of this type each set of companions covers.
0is once per order, however many of this type the order holds. companion.strictnessstringrecommendedshows people a tip and never blocks;requiredrefuses a checkout that falls short — unless no companion can be bought, in which case the rule pauses so this type stays sellable.- one of
recommendedrequired legacy_slugstring- The Tito Pro ticket type's slug, which a website moving from Tito Pro names in
data-tito-q-<slug>to preselect a quantity. Empty for a type made in Tito Go. Carried over by the account import. waitlistboolean- Whether the type opens a waitlist when it is fully booked. The people on it are at GET /events/{slug}/waitlist.
waitlist_head_startinteger- Minutes between one person on the waitlist being emailed about a free place and the next person. 60 unless changed. Only used while
waitlist_modeis first_come. - one of
1530601203601440 waitlist_modestring- What happens when a place becomes free. first_come emails everyone waiting, in the order they joined, a head start apart, and whoever takes the place first gets it. by_hand emails nobody: the free places show on the Waitlist screen and are offered by hand. first_come unless changed.
- one of
first_comeby_hand availableinteger or null- How many a buyer can still buy: the cap less what is sold and held, narrowed by the price ladder, less the places held for the waitlist. The figure the event page shows and an order created through this API is held to. null when the type is uncapped.
waitlist_heldinteger- How many free places are kept off sale for the people on the waitlist: the smaller of the number waiting (or offered a place) and the number free. 0 for a type with no waitlist, a bundle, or a type whose time is picked from a timetable.
sales_start_atstring (RFC 3339) or null- When the type goes on sale, RFC 3339; null for no start. Applies only while
on_saleis true: before it the type is listed andavailableis 0. PATCH takes it back as it is. sales_end_atstring (RFC 3339) or null- When the type comes off sale, RFC 3339; null for no end. The end is exclusive, and from it the type is listed with
available0.
{
"id": 1,
"name": "Early Bird",
"price_cents": 14900,
"currency": "eur",
"prices": {
"eur": 14900,
"gbp": 12900
},
"on_sale": true,
"quantity": 200,
"hide_free": false,
"signup_hide_name": false,
"pay_what_you_want": false,
"suggested_price_cents": 0,
"show_left_at": 0,
"sold": 142,
"description": "Includes lunch on both days.",
"description_text": "Includes lunch on both days.",
"description_html": "Includes \u003cstrong\u003elunch\u003c/strong\u003e on both days.",
"tax_rate_id": 0,
"price_includes_tax": false,
"second_tax_rate_id": 0,
"reverse_charge": "",
"reverse_charge_country": "",
"reverse_charge_note": "",
"color": "teal",
"max_per_order": 0,
"allow_downgrade": false,
"allow_upgrade": true,
"holder_changes_mode": "inherit",
"holder_changes_until": "",
"anonymous": false,
"qr_enabled": true,
"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,
"ticket_type_ids": [
2,
3
],
"quantity": 1,
"per_quantity": 1,
"strictness": "recommended"
},
"legacy_slug": "",
"waitlist": false,
"waitlist_head_start": 60,
"waitlist_mode": "first_come",
"available": 12,
"waitlist_held": 0,
"sales_start_at": "2027-01-01T12:00:00Z",
"sales_end_at": "2027-02-19T12:00:00Z"
}
The OrderItem object
One run of ticket seats. Tickets only — goods, donations and post-order adjustments report through lines (OrderLine).
Attributes
unit_price_centsinteger- Integer cents; see the order's currency. The ticket type's price, also on a seat that was given away free by a group discount or a free-with rule: what was charged is the matching
linesentry'stotal_cents, and itscharged_quantitysays how many were paid for.
{
"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. quantityintegercharged_quantityinteger- How many of
quantitywere paid for, never more thanquantity. Fewer when a group discount or a free-with rule gave some away, and 0 on a ticket that came free with another.total_centsisunit_price_centstimes this, plus any tax added on top;subtotal_centsis the price of allquantitybefore the free ones came off. subtotal_centsinteger- Integer cents, before any tax added on top:
unit_price_centstimesquantity, before any seats given away free (seecharged_quantity). tax_centsinteger- Integer cents. The tax snapshot taken when the line was written; 0 when the line carries no tax.
taxesarray of object- Each tax rate the line charges, with its own share of
tax_cents, which stays the line's whole tax: one entry for a line on one rate, two for a line on two (GST and QST). Absent on a line with 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.
eventstring- The slug of the event this line's ticket type belongs to, present only when that is not the order's own event. A ticket moved to another event keeps its order, and all of its money, at the event it was paid at, so the line recording the move is on that order for a type at the other event. Absent on every other line.
place_releasedboolean- True on an active line whose ticket has moved onto another ticket type or event: its money still counts, and the place it held is now held by the line carrying that ticket's ticket_ref. Absent otherwise.
{
"product_id": 1,
"product": "Early Bird",
"kind": "ticket",
"quantity": 2,
"charged_quantity": 2,
"unit_price_cents": 14900,
"subtotal_cents": 29800,
"tax_cents": 0,
"taxes": [
{
"name": "GST",
"rate_milli_pct": 5000,
"tax_cents": 250
}
],
"total_cents": 29800,
"currency": "eur",
"status": "active",
"tier_id": 0,
"band_id": 0,
"ticket_ref": "",
"event": "",
"place_released": false
}
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.
on_additionboolean- True for a code typed when tickets were added to the order after it was placed. It came off that addition's own payment. Listed once the addition is paid, which is when its lines count towards
amount_cents.
{
"code": "EARLYBIRD",
"amount_cents": 2000,
"currency": "eur",
"on_addition": false
}
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.
completed_atstring (RFC 3339) or null- When the order was finished: paid, placed by bank transfer, or made by the organizer. The moment
order.completedfired. Null until then, and null for an order finished before this field existed. 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.
items[].unit_price_centsinteger- Integer cents; see the order's currency. The ticket type's price, also on a seat that was given away free by a group discount or a free-with rule: what was charged is the matching
linesentry'stotal_cents, and itscharged_quantitysays how many were paid for. 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[].charged_quantityinteger- How many of
quantitywere paid for, never more thanquantity. Fewer when a group discount or a free-with rule gave some away, and 0 on a ticket that came free with another.total_centsisunit_price_centstimes this, plus any tax added on top;subtotal_centsis the price of allquantitybefore the free ones came off. lines[].subtotal_centsinteger- Integer cents, before any tax added on top:
unit_price_centstimesquantity, before any seats given away free (seecharged_quantity). lines[].tax_centsinteger- Integer cents. The tax snapshot taken when the line was written; 0 when the line carries no tax.
lines[].taxesarray of object- Each tax rate the line charges, with its own share of
tax_cents, which stays the line's whole tax: one entry for a line on one rate, two for a line on two (GST and QST). Absent on a line with 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.
lines[].eventstring- The slug of the event this line's ticket type belongs to, present only when that is not the order's own event. A ticket moved to another event keeps its order, and all of its money, at the event it was paid at, so the line recording the move is on that order for a type at the other event. Absent on every other line.
lines[].place_releasedboolean- True on an active line whose ticket has moved onto another ticket type or event: its money still counts, and the place it held is now held by the line carrying that ticket's ticket_ref. Absent otherwise.
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.
discounts[].on_additionboolean- True for a code typed when tickets were added to the order after it was placed. It came off that addition's own payment. Listed once the addition is paid, which is when its lines count towards
amount_cents. 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.
companyobject- Who the order was bought FOR, when the buyer ticked "I'm buying for a company or organization" at checkout. Null on an order somebody placed for themselves, which is most of them — this is the party the invoice is made out to, where
nameandemailare the person who paid. Reconcile a finance system against this, not againstname. company.vat_numberstring- As typed by the buyer, unnormalized and unverified. Empty when they did not give one — it is never required to complete a purchase.
company.address_line1string- The billing address, as one line. Empty when the buyer did not give one.
company.subdivisionstring- State, province or prefecture. Only collected for countries whose addresses carry one, so it is empty for most of Europe even when the rest of the address is filled in. Where Tito has that country's list (the US, Canada, Australia, Brazil, China, India, Indonesia and Mexico) the buyer picks from it and this is the ISO 3166-2 code, so "OH" rather than "Ohio"; elsewhere it is what they typed.
company.po_referencestring- The purchase order number the BUYER issued (BT-13), printed on the invoice. Tito's own order number is BT-14 and is
ref. company.einvoice_idstring- The country-specific routing identifier an e-invoice needs, where the buyer's country has one and the organizer turned
sends_einvoiceson. Read it againstcountry: Italy's is a Codice Destinatario (seven characters) or a PEC address, France's is the buyer's nine-digit SIREN, Germany's is a Leitweg-ID and only a public authority has one. Empty for every other country, because their invoices route onvat_number. Never format-checked at checkout: a wrong value is an invoice that gets re-sent, and refusing the sale would be worse. company.vat_statusstring- What the EU VAT register said about
vat_number. Empty until there is an answer and empty forever for a country the register does not cover. "unavailable" means we could not GET an answer, not that the number is bad: a member state's registry is down several times a month, and reading it as a verdict would act on an outage. - one of
validinvalidunavailable company.vat_registered_namestring- The register's own name for the holder of the number, where the member state returns one. It is evidence that the number belongs to
name; nothing compares the two automatically. Empty when the register gave no name, which some member states never do even for a valid number. buyerobject- What the PERSON said about themselves, as opposed to the company they may have been buying for. Null unless the organizer turned
sends_einvoiceson, which is what puts the country question on the checkout at all. buyer.countrystring- The buyer's own country as ISO 3166-1 alpha-2, upper case. Present on a company order too: with the switch on this is the only country control on the checkout, so it is the country of the purchase either way.
buyer.einvoice_idstring- The buyer's own tax number, where their country wants one on an invoice made out to a person: Italy's codice fiscale, Portugal's NIF, Spain's NIF. Empty for every other country, and always empty when
companyis set, because an invoice to a company carries the company's identifiers rather than the buyer's. PERSONAL DATA in a way a company VAT number is not — a codice fiscale encodes date of birth, sex and birthplace — so handle it like a name or an address, not like a reference code. passed_parametersobject- The allowlisted query params the buyer's link carried, kept on the order. An object rather than a list of pairs because a key appears at most once per order.
{}when the order carries none, never null. Which params are kept is the event'spassed_params_onandpassed_params_pattern. stripe_accountstring- The Stripe account (acct_…) the order's money was paid into. Its refunds go back through that account, whichever account its event pays into now. Empty for an order that never met Stripe.
cardobject- The card that paid the order, as the buyer knows it: the brand and the last four digits, read off the Stripe charge shortly after payment and printed on the receipt as "Visa ending 4242". Null where no card is recorded: an order paid by PayPal or bank transfer, a comp, a free order, or an order paid before Tito started keeping this. Never the full number, the expiry, or anything that could be charged.
card.brandstring- Stripe's own brand code, lower case:
visa,mastercard,amex,discover,diners,jcb,unionpay, and others as Stripe adds them. anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
{
"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",
"completed_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,
"charged_quantity": 2,
"unit_price_cents": 14900,
"subtotal_cents": 29800,
"tax_cents": 0,
"taxes": [
{
"name": "GST",
"rate_milli_pct": 5000,
"tax_cents": 250
}
],
"total_cents": 29800,
"currency": "eur",
"status": "active",
"tier_id": 0,
"band_id": 0,
"ticket_ref": "",
"event": "",
"place_released": false
}
],
"discounts": [
{
"code": "EARLYBIRD",
"amount_cents": 2000,
"currency": "eur",
"on_addition": false
}
],
"price_adjustments": [
{
"label": "Speaker discount",
"amount_cents": -5000,
"currency": "eur"
}
],
"company": {
"name": "Analytical Engines Srl",
"country": "IT",
"vat_number": "IT02659550135",
"address_line1": "Via Ada Byron 12",
"city": "Torino",
"postcode": "10121",
"subdivision": "OH",
"po_reference": "PO-4471",
"einvoice_id": "ABC1234",
"vat_status": "valid",
"vat_registered_name": "ANALYTICAL ENGINES LTD"
},
"buyer": {
"country": "IT",
"einvoice_id": "RSSMRA85T10A562S"
},
"passed_parameters": {
"utm_campaign": "spring",
"utm_source": "newsletter"
},
"stripe_account": "acct_1MEXAMPLE",
"card": {
"brand": "visa",
"last4": "4242"
},
"anonymized_at": null
}
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, at the account's own address (the event's own domain when it has one). Present only while the order is pending. A capability URL: treat it as a secret.
urlstring- Absolute URL of the buyer's order page, at the account's own address (the event's own domain when it has one). 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.
confirmed_atstring (RFC 3339) or null- When the attendee said they are still coming, by pressing an "I'm still coming" button in a message. Null until they do.
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.
duplicateboolean- True when this ticket is valid and its attendee's own email address, ignoring case and surrounding spaces, is on valid tickets in two or more different orders at this event. Tickets in one order sharing an address are not duplicates, and a ticket with no attendee email of its own is never one. The admin attendee list marks the same tickets Duplicate.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
moved_fromobject or null- Where this ticket was before an organizer moved it to this event, null for a ticket that has never moved. A moved ticket keeps its reference, and its order stays at the event it was paid at, so order_ref names an order at that event. A ticket moved more than once says where it came from most recently.
{
"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",
"confirmed_at": null,
"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"
}
],
"duplicate": false,
"anonymized_at": null,
"moved_from": null
}
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 types it lists, in the order it lists them. EMPTY IS EVERYTHING ON SALE, including types made later — not "lists nothing".
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)analyticsboolean- Hands the widget's own events to the tracking already on the organizer's page. Off unless asked for.
all_ticket_types_linkboolean- When the widget lists only some ticket types, its list ends with an "All ticket types" link that reopens it showing every ticket type on sale, keeping the quantities already picked. Off unless asked for.
full_page_linkboolean- Whether the widget's checkout shows an "Open the full page" link in its footer, which carries the buyer to the same step on the event's own Tito page, holding the same order. On unless switched off.
show_promo_fieldboolean- Whether the widget shows the promotion code field, on its ticket list, its order review and the booking form reached from it. Only an event with codes that can be used shows it at all. On unless switched off. The event page's own field is set in Customize.
prefill_reviewboolean- When the website the widget sits on hands over a buyer's details with tito("widget.create", …), whether the widget shows the filled-in form first. Off, the buyer goes straight on to paying once nothing the event asks is missing. Off unless switched on.
openingstring- How the widget opens over the organizer's page. Always
push: the page pushes back as one rounded card and the tickets rise over it. Visitors who ask their device for less motion get a fade. - one of
push interest_messagestringlegacy_tokenstring- The Tito Pro widget token this widget also answers to, so a website moving from Tito Pro keeps the
data-tito-tokenandtito("widget.open", …)it already has. Empty for a widget made in Tito Go. Carried over by the account import and never set through this API. An address the website already prints, not a credential. bannerstring- What the widget shows across the top of its ticket list:
""for no banner,"event"for the event's own banner (followed as it changes), an https image URL, or"asset:<id>", a picture uploaded on the widget's page in the admin. show_dateboolean- Shows the event's date and time above the ticket list, in the event's own time zone. Off by default.
show_venueboolean- Shows the venue's name and address above the ticket list, the address opening a map in a new tab. Off by default.
{
"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",
"analytics": false,
"share_buyer": false,
"all_ticket_types_link": false,
"full_page_link": true,
"show_promo_field": true,
"prefill_review": false,
"opening": "push",
"purpose": "purchase",
"interest_message": "example",
"legacy_token": "",
"banner": "event",
"show_event_name": false,
"show_date": false,
"show_venue": false,
"show_descriptions": true
}
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 sightingsinteger- How many times the widget has loaded there. 0 on a website added with
POST /widget-domainsuntil it first does. first_seen_atstring (RFC 3339) or null- When the widget first loaded there; null on a website added by hand that it hasn't loaded on yet.
last_seen_atstring (RFC 3339) or null- When the widget last loaded there; null on a website added by hand that it hasn't loaded on yet.
decided_atstring (RFC 3339) or null- When somebody answered about this website; null while nobody has.
{
"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",
"decided_at": "2026-08-27T18:20: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",
"share": null
}
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), on an entitlement-scoped list only tickets holding that entitlement, and on a ticket-type-limited list only tickets of those types. 0 when the list has no check-ins yet, never a missing/null value.
ticket_types_limitedboolean- True when the list checks in only the ticket types in ticket_type_ids. It combines with entitlement_id: a ticket must pass both.
ticket_type_idsarray of integer- The ticket types a limited list checks in. Always [] on a list that isn't limited. A limited list with [] here had its only types deleted and checks in nobody.
{
"id": 1,
"name": "Main entrance",
"created_at": "2026-09-01T08:00:00Z",
"checked_in_count": 412,
"entitlement_id": 1,
"ticket_types_limited": true,
"ticket_type_ids": [
12,
14
]
}
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 the ticket types in ticket_type_ids, together, for every per_quantity of this one — or once per order when per_quantity is 0. 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
ticket_type_idsarray of integer- Every companion ticket type, any of which counts. They count together: one of each covers two.
per_quantityinteger- How many of this type each set of companions covers.
0is once per order, however many of this type the order holds. strictnessstringrecommendedshows people a tip and never blocks;requiredrefuses a checkout that falls short — unless no companion can be bought, in which case the rule pauses so this type stays sellable.- one of
recommendedrequired
{
"ticket_type_id": 2,
"ticket_type_ids": [
2,
3
],
"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"
}
The EventCreate object
What POST /events takes.
Attributes
slugstring- The address the event answers on. Derived from the name when blank; lowercase letters, digits and hyphens.
timezonestring- IANA zone the event's dates are read in and shown in. Default
UTC.Localis refused, because it names the server's clock rather than a place. currencystring- Lowercase ISO code; must be one an event can be created in. Default
eur. Fixed after creation. venue_latstring- Latitude as a decimal string from -90 to 90. Latitude and longitude must form a valid pair; empty strings clear the pin.
venue_lngstring- Longitude as a decimal string from -180 to 180. Latitude and longitude must form a valid pair; empty strings clear the pin.
{
"name": "Annual Conference 2027",
"slug": "annual-conference-2027",
"description": "Two days of talks.",
"venue": "The Convention Centre",
"venue_address": "Spencer Dock, Dublin 1",
"timezone": "Europe/Dublin",
"currency": "eur",
"starts_at": "2027-10-02T09:00",
"ends_at": "2027-10-03T17:00",
"all_day": false,
"date_text": "",
"series": "",
"venue_lat": "53.3498",
"venue_lng": "-6.2603",
"venue_place_id": "place-id"
}
The EventPatch object
What PATCH /events/{slug} takes: the General panel's fields, every one optional. Absent means unchanged.
Attributes
slugstring- The event's own address segment. Inside a series the series carries the rest. A new address is lowercase letters, digits and hyphens. The address the event is already at is accepted as it is, so an imported one with a capital letter can be sent back unchanged.
map_providerstringauto(Apple Maps on Apple devices, OpenStreetMap on Linux, Google Maps elsewhere),apple,googleorosm(OpenStreetMap). The map the event page shows first.device, an earlier name forauto, is accepted and stored asauto.- one of
autoapplegoogleosm starts_atstring2006-01-02(all-day) or2006-01-02T15:04in the event's timezone, or RFC 3339 with an offset (the form the read returns);""for none.ends_atstring2006-01-02(all-day) or2006-01-02T15:04in the event's timezone, or RFC 3339 with an offset (the form the read returns);""for none.unlistedboolean- Off the listings, and nothing closes: anyone with the link can open the page and buy.
venue_latstring- Latitude as a decimal string from -90 to 90. Latitude and longitude must form a valid pair; empty strings clear the pin.
venue_lngstring- Longitude as a decimal string from -180 to 180. Latitude and longitude must form a valid pair; empty strings clear the pin.
passed_params_onboolean- Turn keeping a buyer's link parameters on or off. This is the only way off: writing an empty
passed_params_patternresets to the default instead. passed_params_patternstring- Comma-separated globs, anchored at both ends, lowercased. A wildcard needs at least two literal characters in front of it, so
*anda*are refused with 422. An empty string resets toutm_*. calendar_on_event_pageboolean- Turn Add to calendar on the event page, before anybody books, on or off. It does not change the offer after booking, which is
calendar_links_enabledon/events/{slug}/settings/after-registering. secret_redirect_urlstring- Where the event's page sends visitors while the event is secret. An
httpsaddress, or an empty string for none. Anything else is refused with 422. Kept when the event is made public.
{
"name": "Annual Conference 2027",
"slug": "annual-conference-2027",
"description": "Two days of talks.",
"venue": "The Convention Centre",
"venue_address": "Spencer Dock, Dublin 1",
"map_provider": "google",
"timezone": "Europe/Dublin",
"starts_at": "2027-10-02T09:00",
"ends_at": "2027-10-03T17:00",
"all_day": false,
"date_text": "",
"secret": false,
"unlisted": false,
"series": "",
"share_title": "Two days of talks in Dublin",
"share_text": "Tue 10 Nov 2026 at City Hall. Doors at 18:00.",
"venue_lat": "53.3498",
"venue_lng": "-6.2603",
"venue_place_id": "place-id",
"search_indexing": "inherit",
"passed_params_on": true,
"passed_params_pattern": "utm_*,gclid",
"calendar_on_event_page": true,
"secret_redirect_url": "https://example.com/tickets"
}
The EventDuplicate object
What POST /events/{slug}/duplicate takes.
Attributes
slugstring- The copy's own address segment; derived from the name when blank, shortened to 60 characters, and a generated address like
event-2when the name has no Latin spelling. The copy inherits its source's series. starts_onstring- The copy's start date,
2006-01-02; the clock comes from the source. Blank keeps the source's. shift_datesboolean- Move every dated thing inside the event by the weeks the event moved. Default false: copy them as they are.
{
"name": "Annual Conference 2028",
"slug": "annual-conference-2028",
"starts_on": "2028-10-07",
"ends_on": "2028-10-08",
"shift_dates": true,
"coupons": true,
"portals": true,
"invitations": true,
"reports": true,
"downloads": true
}
The SeriesCreate object
What POST /series takes.
Attributes
{
"name": "Meet Tito",
"slug": "meet-tito",
"intro": "Monthly, somewhere in Europe.",
"listed": true,
"show_past": true
}
The SeriesPatch object
What PATCH /series/{slug} takes; every field optional, absent means unchanged.
Attributes
{
"name": "Meet Tito",
"slug": "meetups",
"intro": "Monthly, somewhere in Europe.",
"listed": true,
"show_past": true
}
The TicketTypeCreate object
What POST /events/{slug}/ticket-types takes.
Attributes
descriptionstring- Shown under the name on the event page. Plain text, or a composer body carrying bold, italic, links (https or mailto) and bullets. Anything else in a body is kept as its words.
price_includes_taxboolean- Whether
price_centsalready includes the tax. Meaningless, and stored false, with no rate. second_tax_rate_idinteger- A second of the account's tax rates, charged beside
tax_rate_idon the same price, or 0 for none. Needs atax_rate_id, and may not be the same rate. Default 0. pay_what_you_wantboolean- Let buyers choose the price. price_cents is then the minimum, and may be 0 to let people come for free.
suggested_price_centsinteger- With pay_what_you_want, the price the buyer's box starts on, 0 for none. Never less than price_cents (422).
pricesobject- Price per currency, lowercase code → cents. The event's own currency, if present, must equal
price_cents. On a PATCH,nullclears a currency (the type is then not offered there) and an absent key is unchanged.
{
"name": "Early bird",
"price_cents": 1500,
"quantity": 50,
"description": "The first fifty.",
"tax_rate_id": 0,
"price_includes_tax": true,
"second_tax_rate_id": 0,
"pay_what_you_want": false,
"suggested_price_cents": 0,
"prices": {
"gbp": 1700
}
}
The TicketTypePatch object
What PATCH /events/{slug}/ticket-types/{id} takes: both panels' fields, every one optional.
Attributes
price_centsinteger- The price in the event's currency. Refused on a laddered, banded or bundled type.
quantityinteger or null- Capacity; null is uncapped. Refused on a laddered, banded or bundled type.
descriptionstring- Shown under the name on the event page. Plain text, or a composer body carrying bold, italic, links (https or mailto) and bullets. Anything else in a body is kept as its words.
second_tax_rate_idinteger- A second of the account's tax rates, charged beside
tax_rate_idon the same price, or 0 for none. Needs atax_rate_id, and may not be the same rate; settingtax_rate_idto 0 takes it off too. reverse_chargestring""for nobody,country(withreverse_charge_country), oreu. Any of the three fields sent restates the rest from what is stored.pricesobject- Price per currency, lowercase code → cents. The event's own currency, if present, must equal
price_cents. On a PATCH,nullclears a currency (the type is then not offered there) and an absent key is unchanged. allow_downgradeboolean- Whether this type is ticked under the event's
allow_downgrades. A buyer may change a ticket of this type down to a cheaper one, and get the difference back, when both are on. allow_upgradeboolean- Whether this type is ticked under the event's
allow_upgrades. A buyer may change a ticket of this type up to a dearer one, and pay the difference, when both are on. holder_changes_modestringinheritthe event's rule, orallow/denyholder changes on this type.- one of
inheritallowdeny holder_changes_untilstring- The deadline for holder changes when the mode is allow, as the form stores it; cleared when the mode is inherit.
hide_freeboolean- Leave the word Free off this ticket on the event page and the widget. Only a free ticket with no price ladder can be set to true (422 otherwise): a price is never hidden.
signup_hide_nameboolean- Leave this ticket's name and price off a free event's sign-up form. Only a free ticket with no price ladder can be set to true (422 otherwise).
pay_what_you_wantboolean- Let buyers choose the price, with price_cents as the minimum. Refused with 409 pricing_mode on a ticket priced by tiers, volume prices or a bundle, and with 409 in_bundle on a ticket that is part of a bundle. While it is true, a PUT to the ticket type's tiers, bands or bundle answers 409 pricing_mode. false takes the suggestion off too.
suggested_price_centsinteger- The price the buyer's box starts on, 0 for none. Needs pay_what_you_want, and is never less than price_cents (422).
show_left_atinteger- 0 (never) or how few of this ticket have to be left before the event page and the widget say how many.
waitlistboolean- Open a waitlist when the type is fully booked. Switching it off keeps the people already on the list. A bundle cannot have a waitlist: switching one on for a bundle is refused. Neither, for now, can a type whose time is picked from a timetable on the event page: switching one on for it is refused too, and a list switched on before the timetable was added keeps taking names but holds no place and offers none.
waitlist_head_startinteger- Minutes between one person on the waitlist being emailed about a free place and the next person. Any other value is refused.
- one of
1530601203601440 waitlist_modestring- What happens when a place becomes free: first_come emails everyone waiting, a head start apart; by_hand emails nobody until a place is offered by hand. Any other value is refused.
- one of
first_comeby_hand sales_start_atstring or null- When the type goes on sale: on the event's clock (
2027-01-01T12:00), or RFC 3339 as the read answers; "" or null for no start. It applies only while the type is on sale. Sent alone, it is checked against the end as it stands. sales_end_atstring or null- When the type comes off sale: on the event's clock, or RFC 3339 as the read answers; "" or null for no end. It must come after
sales_start_at.
{
"name": "Early bird",
"price_cents": 1700,
"quantity": 60,
"description": "The first sixty.",
"tax_rate_id": 0,
"price_includes_tax": true,
"second_tax_rate_id": 0,
"reverse_charge": "country",
"reverse_charge_country": "ES",
"reverse_charge_note": "",
"prices": {
"gbp": 1700
},
"allow_downgrade": false,
"allow_upgrade": true,
"color": "teal",
"holder_changes_mode": "inherit",
"holder_changes_until": "",
"anonymous": false,
"qr_enabled": true,
"max_per_order": 0,
"hide_free": false,
"signup_hide_name": false,
"pay_what_you_want": true,
"suggested_price_cents": 2500,
"show_left_at": 0,
"waitlist": true,
"waitlist_head_start": 60,
"waitlist_mode": "by_hand",
"sales_start_at": "2027-01-01T12:00",
"sales_end_at": "2027-02-19T12:00"
}
The TicketTypeRemoved object
What DELETE /events/{slug}/ticket-types/{id} did.
Attributes
archivedboolean- The type had been used and is archived instead: out of the catalogue and off sale, orders and tickets untouched.
{
"id": 7,
"deleted": false,
"archived": true
}
The Field object
A field of the account's library: a question asked of people, or metadata recorded about them.
Attributes
kindstring- What kind of answer it takes.
- one of
texttextareaselectmulti_selectcheckboxyes_nodatenumberphonecountryfile optionsarray of string- The choices of a select or multi_select, in order, as stored. Empty on every other kind.
modestringwritable: attendees answer it and see it (a question).readonly: organizers record it, attendees see it.secret: organizers record it, only they see it.- one of
writablereadonlysecret max_filesinteger- How many files one person may upload on a file field (1–10); 1 on every other kind.
other_choiceboolean- A select or multi_select ends in a write-in answer: what the person writes is stored as the answer itself. false on every other kind.
other_labelstring- The organizer's own word for the write-in row; empty means the standard “Other” in the buyer's language.
country_onlyarray of string- country fields only: the countries offered, as ISO 3166-1 alpha-2 codes; empty offers every country. A country field's answers are these codes too. Empty on every other kind.
country_pinnedarray of string- country fields only: the countries that head the list, in this order, as ISO 3166-1 alpha-2 codes. A pinned country is always offered. Empty on every other kind.
{
"id": 12,
"key": "t-shirt-size",
"label": "T-shirt size",
"kind": "select",
"options": [
"S",
"M",
"L"
],
"scope": "person",
"mode": "writable",
"description": "For the goody bag.",
"max_files": 1,
"other_choice": false,
"other_label": "",
"country_only": [],
"country_pinned": [
"AE",
"SA",
"GB",
"IN"
],
"position": 3,
"archived": false
}
The FieldCreate object
What POST /fields takes.
Attributes
kindstring- What kind of answer it takes.
- one of
texttextareaselectmulti_selectcheckboxyes_nodatenumberphonecountryfile optionsarray of string- The choices of a select or multi_select, in order, as stored. Empty on every other kind.
country_onlyarray of string- country fields only: the countries offered, as ISO 3166-1 alpha-2 codes; empty offers every country. A country field's answers are these codes too.
country_pinnedarray of string- country fields only: the countries that head the list, in this order, as ISO 3166-1 alpha-2 codes. A pinned country is always offered.
{
"label": "T-shirt size",
"kind": "select",
"key": "",
"options": [
"S",
"M",
"L"
],
"mode": "writable",
"description": "For the goody bag.",
"max_files": 1,
"other_choice": false,
"other_label": "",
"country_only": [],
"country_pinned": [
"AE",
"SA",
"GB",
"IN"
]
}
The FieldPatch object
What PATCH /fields/{id} takes; every field optional, absent means unchanged.
Attributes
kindstring- Changes the field's type (never to or from file; the kind the field already has is accepted and changes nothing). Runs in its own step, after the checks that need only this request (label, mode, max_files, the country lists, other_choice and other_label) and before the rest of the save: every answer already given is checked against the new kind, and a select or multi_select target takes
optionsfrom this same request if given, else the field's own stored choices, cleaned the same way (trimmed, blanks dropped, exact duplicates dropped keeping first-seen order). - one of
texttextareayes_noselectmulti_selectcheckboxdatenumberphonecountry optionsarray of string- The choices of a select or multi_select, in order, as stored. Empty on every other kind.
other_labelstring- select and multi_select only: the organizer's own word for the write-in row; empty for the standard “Other”. Cleared when other_choice is false.
country_onlyarray of string- country fields only: the countries offered, as ISO 3166-1 alpha-2 codes; empty offers every country. A country field's answers are these codes too. Replaces the whole list when sent.
country_pinnedarray of string- country fields only: the countries that head the list, in this order, as ISO 3166-1 alpha-2 codes. A pinned country is always offered. Replaces the whole list when sent.
{
"label": "T-shirt size",
"kind": "textarea",
"options": [
"S",
"M",
"L"
],
"mode": "writable",
"description": "For the goody bag.",
"max_files": 3,
"other_choice": true,
"other_label": "Something else",
"country_only": [],
"country_pinned": [
"AE",
"SA",
"GB",
"IN"
]
}
The FieldRestore object
What POST /fields/{id}/restore takes, optionally.
Attributes
labelstring- A new label for the field on its way back; required when a live field already carries the old one.
{
"label": "Company (2024)"
}
The FieldRemoved object
What DELETE /fields/{id} did.
Attributes
{
"id": 12,
"deleted": true,
"archived": false
}
The FieldAttachment object
One place a field is asked or recorded on an event.
Attributes
condition_field_idintegercondition_valuestringcondition_rulesarray of objectcondition_rules[].parent_idintegercondition_rules[].valuesarray of stringtarget_typestring- Where it is asked.
orderis the order form, target_id 0. - one of
orderticket_typeitemdonationportalapplicationsessiongoods_groupwaitlist excludedboolean- A portal row that is an exception to "asked on every portal" rather than an attachment.
{
"condition_field_id": 1,
"condition_value": "example",
"condition_rules": [
{
"parent_id": 1,
"operator": "equals",
"values": [
"example"
]
}
],
"id": 3,
"field_id": 12,
"field_label": "T-shirt size",
"target_type": "ticket_type",
"target_id": 7,
"target_name": "VIP",
"required": true,
"position": 0,
"label_override": "",
"excluded": false
}
The FieldAttach object
What POST /events/{slug}/fields takes.
Attributes
target_typestring- Where to ask it.
- one of
orderticket_typeitemdonationportalapplicationsessiongoods_groupwaitlist
{
"field_id": 12,
"target_type": "ticket_type",
"target_id": 7,
"required": true
}
The Coupon object
A discount rule.
Attributes
amounts_offobject- Fixed amount per currency, lowercase code → cents. Empty on a percent coupon. On a write, the event's own currency must equal
amount_off_cents;nullclears a currency. applicabilitystringticket: off each eligible ticket.order: off the order once.- one of
ticketorder roundingstring- How a percentage settles the part of a cent it comes to, on each ticket.
downunless the coupon came from Tito Pro (half_up) or Tito Classic (half_down), or was edited from one, so it takes off what it took off there. Read-only. - one of
downhalf_uphalf_down limit_unitstring- What one use of the coupon's limited codes is:
orders, ortickets(a coupon applied to each ticket only). Withtickets, a code with 10 uses discounts 10 tickets over any number of orders, and the order that crosses the limit gets the discount on as many tickets as are left. It can't change once a code has been used. - one of
orderstickets combinableboolean- Whether the coupon's codes can share an order with any other code.
false: a code of it can only be the one code on an order, and a buyer adding a second code is refused. It isn't part of the price, so it can change at any time and rules checkouts from then on. applies_on_volume_pricesboolean- Whether the coupon also comes off a ticket the order has taken enough of for one of its volume prices.
false: those tickets are charged the volume price with nothing off, and the order's other tickets keep the discount. It is part of the price and rules checkouts from then on: an order already taken keeps what it was charged. shows_ticket_type_idsarray of integer- Ticket types kept off the event page that a buyer who applies one of the coupon's codes sees there too, in their usual place. Empty shows none.
shows_after_used_upstring- What the shown ticket types do once a code's uses run out:
hidethem again, orkeepshowing them at full price. - one of
hidekeep shows_only_its_ticketsboolean- Whether a buyer who applies one of the coupon's codes sees only the ticket types in
ticket_type_ids, with the event page's other tickets hidden. Always false whenticket_type_idsis empty. min_per_ticket_typeinteger- How many of one ticket type an order needs before the coupon comes off that type. Each type is counted on its own. 0 is no minimum.
max_per_ticket_typeinteger- How many of each ticket type in an order the coupon comes off. Any more of that type are full price. 0 is no maximum.
created_atstring (RFC 3339) or null
{
"id": 4,
"name": "Early bird",
"kind": "percent",
"percent_bps": 2500,
"amount_off_cents": 0,
"currency": "eur",
"amounts_off": {
"eur": 500,
"gbp": 400
},
"applicability": "ticket",
"rounding": "down",
"limit_unit": "orders",
"combinable": true,
"applies_on_volume_prices": true,
"shows_ticket_type_ids": [
9
],
"shows_after_used_up": "hide",
"shows_only_its_tickets": false,
"min_per_ticket_type": 0,
"max_per_ticket_type": 0,
"ticket_type_ids": [
7
],
"starts_at": "2027-01-01T00:00:00Z",
"ends_at": null,
"created_at": "2026-09-07T18:20:00Z",
"codes": 3
}
The CouponInput object
What POST /events/{slug}/coupons and PATCH …/{id} take. Give one of percent_bps and amount_off_cents, or both at 0 with shows_ticket_type_ids for a coupon that only shows hidden tickets; on a PATCH every field is optional.
Attributes
amounts_offobject- Fixed amount per currency, lowercase code → cents. Empty on a percent coupon. On a write, the event's own currency must equal
amount_off_cents;nullclears a currency. limit_unitstring- What one use of the coupon's limited codes is:
orders, ortickets(a coupon applied to each ticket only). Withtickets, a code with 10 uses discounts 10 tickets over any number of orders, and the order that crosses the limit gets the discount on as many tickets as are left. It can't change once a code has been used. - one of
orderstickets combinableboolean- Default
true.falselets a code of this coupon be only the one code on an order. Imported Tito Classic coupons startfalse. applies_on_volume_pricesboolean- Default
true.falsetakes nothing off a ticket the order has taken enough of for one of its volume prices. Absent on a PATCH leaves it as it is. shows_ticket_type_idsarray of integer- Ticket types of this event, kept off its event page, that a buyer who applies one of the coupon's codes sees there too. Empty shows none; absent leaves them as they are.
shows_after_used_upstring- Default
hide. What the shown ticket types do once a code's uses run out:hidethem again, orkeepshowing them at full price. - one of
hidekeep shows_only_its_ticketsboolean- Default
false. Whether a buyer who applies one of the coupon's codes sees only the ticket types inticket_type_ids, with the event page's other tickets hidden. Needsticket_type_ids. Left out, it stays as it is. min_per_ticket_typeinteger- Default
0, no minimum. How many of one ticket type an order needs before the coupon comes off that type, 1 to 1000000. For a coupon applied to each ticket only. Left out, it stays as it is. max_per_ticket_typeinteger- Default
0, no maximum. How many of each ticket type in an order the coupon comes off, 1 to 1000000. It may be belowmin_per_ticket_type: a minimum of 5 and a maximum of 2 discounts 2 of an order's 5 or more. For a coupon applied to each ticket only. Left out, it stays as it is.
{
"name": "Early bird",
"percent_bps": 2500,
"amount_off_cents": 500,
"amounts_off": {
"eur": 500,
"gbp": 400
},
"applicability": "ticket",
"limit_unit": "orders",
"combinable": true,
"applies_on_volume_prices": true,
"shows_ticket_type_ids": [
9
],
"shows_after_used_up": "hide",
"shows_only_its_tickets": false,
"min_per_ticket_type": 2,
"max_per_ticket_type": 5,
"ticket_type_ids": [
7
],
"starts_at": "2027-01-01T00:00:00Z",
"ends_at": "2027-06-01T00:00:00Z"
}
The PromoCode object
One redeemable code of a coupon.
Attributes
{
"id": 9,
"coupon_id": 4,
"code": "SPRING25",
"max_redemptions": 100,
"redemptions": 12,
"label": "Newsletter",
"batch_id": 0,
"created_at": "2026-09-07T18:21:00Z",
"retired": false
}
The PromoCodeInput object
What POST …/codes takes.
Attributes
codestring- 1–191 characters with no whitespace and none of
/ & % # , ; "; case-sensitive; unique per event among live codes.
{
"code": "SPRING25",
"max_redemptions": 100,
"label": "Newsletter"
}
The PromoBatchInput object
What POST …/codes/batch takes.
Attributes
{
"count": 50,
"prefix": "SPON-",
"max_redemptions": 1,
"label": "Sponsor"
}
The PromoLabelInput object
What PATCH …/codes/{cid} takes.
Attributes
{
"label": "Print run"
}
The Message object
A message from the organizer to the people of an event.
Attributes
bodystring- The body as prose: what was typed, with the composer's variables read as their fallbacks and its blocks and marks dropped.
body_sourcestring- The stored source as the composer keeps it, variables, blocks and marks included. Read-only.
previewstring- The line an inbox shows after the subject: plain words on one line, hidden in the email itself. Empty for none.
audiencestring- Who it goes to:
all,buyers,both,invited,waitlist,signups, ortype:<ticket type id>.waitlistis everyone on the event's waitlists except those who left, and each copy ends with that person's own link to leave.signupsis everyone who confirmed a sign-up on one of the event's widgets that collect emails, never anyone who withdrew; it takes no segment, and each copy ends with that person's own link to stop updates. segmentobject or null- The compose page's own filters, as it stores them (checked_in, confirmed, since, type, ostatus, …); null for none.
confirmedisyesorno: whether the attendee has said they are still coming. A since date begins at midnight in the event's timezone.pickedis a hand-picked set of ticket references, comma-separated, as the attendee list sends ticked rows. It narrows the ticket door to those tickets and the order door to whoever booked them, and unlike every other filter the set does not grow, so a scheduled message goes to the same people however the event changes. It must be a string of at most 2000 references, or the request is refused with 422invalid_body. Apickedvalue with no readable reference in it matches nobody. calendarboolean- Whether the message carries a calendar file for the event. Each recipient's copy is built from their own order, so somebody holding a timed ticket gets their session as well as the event.
filesarray of MessageFile- Files attached to the message, in order. Read only: attaching one is a browser upload.
files[].filenamestringfiles[].deliverystringemailwhen the file is sent with the message,linkwhen the email carries a download link to it instead,offlinewhen Tito took the file offline and it is sent neither way. Decided by the weight of everything attached, in order. An offline file adds no weight.- one of
emaillinkoffline statusstringdraft,scheduled,sending(on its hold, or going out),sent, orstopped(a sweep declined; seestop_reason).- one of
draftscheduledsendingsentstopped deliveredinteger- Sent emails the email provider reports as delivered to the recipient's mail server.
waitinginteger- Sent emails the provider hasn't reported on yet, usually for seconds. One it is still trying (a full mailbox, a slow server) waits up to a day before it counts as bounced.
bouncedinteger- Sent emails the recipient's mail server turned away: the address doesn't exist, or a full mailbox stayed full for a day.
not_sentinteger- Emails the provider didn't send, because an earlier email to the address bounced or was reported as spam.
marked_as_spaminteger- Sent emails the recipient reported as spam. Whatever of
sentthese five leave was sent through a mail transport that can't report back (SMTP, or a sender imported from Tito Classic or Pro). created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or nullemail_sender_address_idintegeremail_sender_address_idis the ID of the selected address, or0for a domain choice or a message that follows the event. When it is0, a positiveemail_sender_ididentifies the chosen domain;email_sender_id: 0means follow the event.email_sender_idinteger- The sender chosen for this message (
GET /email-senders), 0 when it sends from its event's sender. This is the sender asked for: one that is not live when the message goes is replaced by the event's.
{
"id": 3,
"subject": "Doors open at 8",
"body": "See you there.\n\nBring ID.",
"body_source": "See you there.\n\nBring ID.",
"preview": "Bring your ticket and a jacket",
"audience": "all",
"segment": {
"checked_in": "no"
},
"look": "",
"calendar": false,
"files": [
{
"filename": "floor-plan.pdf",
"size": 284193,
"delivery": "email"
}
],
"status": "draft",
"scheduled_for": null,
"send_after": null,
"scheduled_count": 0,
"sent": 0,
"failed": 0,
"pending": 0,
"logged": 0,
"delivered": 401,
"waiting": 0,
"bounced": 6,
"not_sent": 3,
"marked_as_spam": 2,
"stop_reason": "",
"scheduled_by": "",
"stopped_by": "",
"created_at": "2026-09-07T20:50:00Z",
"updated_at": "2026-09-07T20:50:00Z",
"email_sender_address_id": 1,
"email_sender_id": 0,
"kind": "bulk"
}
The MessageInput object
What POST /events/{slug}/messages and PATCH …/{id} take; every field optional on a PATCH, and a draft needs a subject, a body or a preview.
Attributes
previewstring- The line an inbox shows after the subject. Plain words; line breaks become spaces. Empty for none.
segmentobject or null- The compose page's own filters, as it stores them (checked_in, confirmed, since, type, ostatus, …); null for none.
confirmedisyesorno: whether the attendee has said they are still coming. A since date begins at midnight in the event's timezone.pickedis a hand-picked set of ticket references, comma-separated, as the attendee list sends ticked rows. It narrows the ticket door to those tickets and the order door to whoever booked them, and unlike every other filter the set does not grow, so a scheduled message goes to the same people however the event changes. It must be a string of at most 2000 references, or the request is refused with 422invalid_body. Apickedvalue with no readable reference in it matches nobody. A key the compose page has no filter for, or a value of the wrong type (typeis a ticket type's id as a number, not a string), is refused with 422invalid_bodynaming it, rather than stored as no narrowing and sent to the whole audience. email_sender_address_idintegeremail_sender_address_idis optional. A positive ID selects that exact address from theaddressesreturned byGET /email-senders. Its sender must be live; a saved message can keep its unchanged choice while the sender is pending. If both IDs are positive, the address must belong toemail_sender_id, or the request returns HTTP 422invalid_body. Address ID0follows the event unless a positiveemail_sender_idselects a domain. Omitting both fields from a messagePATCHpreserves its choice. A domain-onlyPATCHrepeating the current domain also preserves a selected address. To select the domain explicitly, send its ID with address ID0.email_sender_idinteger- The sender this message goes from: one of the account's live senders (
GET /email-senders), or 0 to send from the event's sender. Left out on a create, it is 0; left out on a PATCH, it is unchanged. Any other id is refused with422 invalid_body, except the message's own current choice.POST …/{id}/sendsends from what is stored.
{
"subject": "Doors open at 8",
"body": "See you there.\n\nBring ID.",
"preview": "Bring your ticket and a jacket",
"audience": "buyers",
"segment": {
"checked_in": "no"
},
"look": "plain",
"calendar": true,
"email_sender_address_id": 1,
"email_sender_id": 2
}
The MessageSendInput object
What POST …/{id}/send takes, optionally.
Attributes
atstring- When to send, RFC 3339, in the future and at most six months out. Absent means now, after the sixty-second hold.
{
"at": "2026-10-01T09:00:00Z"
}
The InvitationList object
A set of ticket types handed out by invitation.
Attributes
{
"id": 5,
"name": "Speakers",
"ticket_type_ids": [
7,
8
],
"default_max_tickets": 2,
"default_edit_details": false,
"claim_deadline": null,
"archived": false,
"created_at": "2026-09-07T21:20:00Z"
}
The InvitationListInput object
What POST /events/{slug}/invitation-lists and PATCH …/{id} take; on a PATCH every field is optional and the two defaults are refused.
Attributes
namestring
{
"name": "Speakers",
"ticket_type_ids": [
7,
8
],
"default_max_tickets": 2,
"default_edit_details": false,
"claim_deadline": "2027-01-01T00:00:00Z"
}
The Invitation object
One person's invitation on a list.
Attributes
email_historyarray of object- When available, the invitation detail response includes
email_historyrecords withid,message_id,subject,recipient,cc(the copies that email went to, comma-separated),status, andcreated_atin Unix seconds; list responses omit this field. email_history[].idintegeremail_history[].message_idintegeremail_history[].subjectstringemail_history[].recipientstringemail_history[].ccstringemail_history[].statusstringemail_history[].created_atintegerccarray of string- Who the invitation copies in, lowercased. They get every email the person gets, on a visible Cc line. Empty when nobody is.
edit_detailsboolean- Whether they may change the name and email typed for them; always true on an open link.
statestring- The person's own answer, or the organizer's decision on a registration that needs approval (awaiting_approval, rejected). Only open and no can still be claimed.
- one of
openyesnopayment_pendingcanceledawaiting_approvalrejected statusstring- The computed
statusfield emitsdraft,sent,claimed,declined,payment pending,canceled,expired,revoked,waiting on you, ornot accepted.draftandsentreplacepending. The stored invitation state remains unchanged, such asopen.sentconfirms a successful send, not inbox delivery. - one of
draftsentclaimeddeclinedpayment pendingcanceledexpiredrevokedwaiting on younot accepted ticket_type_idsarray of integer- This person's narrowing of the list's offer; empty means whatever the list offers.
deadlinestring (RFC 3339) or null- When this invitation stops working: its own, else the list's; null for none.
revokedbooleancreated_atstring (RFC 3339) or nullduplicateboolean- True when another invitation on this event has the same email address, ignoring case and surrounding spaces. Always false on an open link with no email. The admin invitation list marks the same invitations Duplicate.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
answered_forobject or null- The organizer's answer while it stands: an invitation claimed or declined for the invitee with Claim for them or Decline for them, or with POST …/claim and …/decline. Null when the invitee answered themselves, or nobody has. Once the invitee changes their answer, it is null again.
answered_for.atstring (RFC 3339)
{
"email_history": [
{
"id": 1,
"message_id": 1,
"subject": "example",
"recipient": "example",
"cc": "example",
"status": "ticket",
"created_at": 1
}
],
"id": 31,
"list_id": 5,
"url": "https://acct.go.tito.io/annual/invitations/8f3k…",
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"state": "open",
"status": "draft",
"ticket_type_ids": [],
"deadline": null,
"revoked": false,
"sent_at": null,
"last_sent_at": null,
"order_ref": "",
"created_at": "2026-09-07T21:21:00Z",
"duplicate": false,
"anonymized_at": null,
"answered_for": {
"answer": "declined",
"at": "2026-10-23T14:00:00Z",
"by": "you@example.com",
"reason": "Replied to the invitation email"
}
}
The InvitationInput object
What POST …/{list}/invitations and PATCH /invitations/{id} take; every field optional.
Attributes
{
"name": "Ada Lovelace",
"email": "ada@example.com",
"cc": [
"office@example.com"
],
"max_tickets": 2,
"edit_details": false,
"ticket_type_ids": [
7
],
"deadline": "",
"list_id": 5
}
The OrderConfirmationResendInput object
What POST /orders/{ref}/confirmation takes, optionally. Anything left out is the confirmation as it stands.
Attributes
notestring- Under the greeting, above the rest of the email, in the message composer's source: plain text with a blank line between paragraphs,
{{ mark.bold }}style marks for formatting, and details such as{{ person.first_name }}or{{ order.reference }}, resolved for the buyer. Up to 2000 characters; a longer note is refused with422 note_too_long. It goes with this send only. email_sender_address_idintegeremail_sender_address_idis optional. A positive ID selects that exact address from theaddressesreturned byGET /email-senders. Its sender must be live; a saved message can keep its unchanged choice while the sender is pending. If both IDs are positive, the address must belong toemail_sender_id, or the request returns HTTP 422invalid_body. Address ID0follows the event unless a positiveemail_sender_idselects a domain. Omitting both fields from a messagePATCHpreserves its choice. A domain-onlyPATCHrepeating the current domain also preserves a selected address. To select the domain explicitly, send its ID with address ID0.email_sender_idinteger- The sender to send from: one of the account's live senders (
GET /email-senders). 0 or left out sends from the event's sender. Any other id is refused with422 invalid_body. A chosen sender that stops being live before the email goes is replaced by the event's sender.
{
"subject": "Your tickets for Annual Conference",
"preview": "Doors open at seven",
"note": "Doors open at seven.",
"email_sender_address_id": 1,
"email_sender_id": 2
}
The TicketResendInput object
What POST …/tickets/{ref}/resend takes, optionally. Anything left out is the ticket email as it stands.
Attributes
notestring- Under the greeting, above the rest of the email, in the message composer's source: plain text with a blank line between paragraphs,
{{ mark.bold }}style marks for formatting, and details such as{{ person.first_name }}or{{ ticket.type }}, resolved for the ticket's holder. Up to 2000 characters; a longer note is refused with422 note_too_long. It goes with this send only. email_sender_address_idintegeremail_sender_address_idis optional. A positive ID selects that exact address from theaddressesreturned byGET /email-senders. Its sender must be live; a saved message can keep its unchanged choice while the sender is pending. If both IDs are positive, the address must belong toemail_sender_id, or the request returns HTTP 422invalid_body. Address ID0follows the event unless a positiveemail_sender_idselects a domain. Omitting both fields from a messagePATCHpreserves its choice. A domain-onlyPATCHrepeating the current domain also preserves a selected address. To select the domain explicitly, send its ID with address ID0.email_sender_idinteger- The sender to send from: one of the account's live senders (
GET /email-senders). 0 or left out sends from the event's sender. Any other id is refused with422 invalid_body. A chosen sender that stops being live before the email goes is replaced by the event's sender.
{
"subject": "Your ticket for Annual Conference",
"preview": "Doors open at seven",
"note": "Bring photo ID to the door.",
"email_sender_address_id": 1,
"email_sender_id": 2
}
The InvitationSendInput object
What POST …/{id}/send takes, optionally: the words, or the account's stock wording when left out.
Attributes
subjectstringpreviewstring- The line an inbox shows after the subject. Plain words; never filled in from the stock wording.
email_sender_address_idintegeremail_sender_address_idis optional. A positive ID selects that exact address from theaddressesreturned byGET /email-senders. Its sender must be live; a saved message can keep its unchanged choice while the sender is pending. If both IDs are positive, the address must belong toemail_sender_id, or the request returns HTTP 422invalid_body. Address ID0follows the event unless a positiveemail_sender_idselects a domain. Omitting both fields from a messagePATCHpreserves its choice. A domain-onlyPATCHrepeating the current domain also preserves a selected address. To select the domain explicitly, send its ID with address ID0.email_sender_idinteger- The sender to send from: one of the account's live senders (
GET /email-senders). 0 or left out sends from the event's sender. Any other id is refused with422 invalid_body. A chosen sender that stops being live before the email goes is replaced by the event's sender.
{
"subject": "You're invited to Annual Conference",
"body": "We'd love to have you.",
"preview": "Black tie, eight sharp",
"email_sender_address_id": 1,
"email_sender_id": 2
}
The Audience object
A named public booking page showing some of the event's ticket types.
Attributes
namestringshow_bank_transferboolean- Whether this page's buyers are offered bank transfer, when the event offers it. Defaults to on when an audience is created.
show_express_checkoutboolean- Whether this page's buyers are offered express checkout: Apple Pay, Google Pay, Link and PayPal. Card payment is offered either way. Defaults to on when an audience is created. The event's own
express_checkout_enabledoff wins over this. organizations_onlyboolean- Whether every booking on this page is made out to an organization: the checkout asks for the organization's name, always, with no checkbox, even where the event does not ask about companies. Defaults to off.
ask_vatboolean- Whether the checkout's organization details ask for a VAT number. Off means no box, and a VAT number posted anyway is not stored. Defaults to on.
countriesarray of string- The countries this page's buyers can be in, as ISO 3166-1 alpha-2 codes; empty for any. The checkout offers only these, and with exactly one it does not ask: that country is stored on every order.
created_atstring (RFC 3339) or null
{
"id": 6,
"slug": "press",
"url": "https://acct.go.tito.io/annual/press",
"name": "Press",
"description": "For accredited press.",
"max_per_order": 2,
"show_promo_field": true,
"show_bank_transfer": true,
"show_express_checkout": true,
"show_quote_button": false,
"ref_prefix": "PR",
"organizations_only": false,
"ask_vat": true,
"countries": [
"IE",
"GB"
],
"ticket_type_ids": [
8
],
"closed": false,
"created_at": "2026-09-07T21:33:00Z"
}
The AudienceInput object
What POST /events/{slug}/audiences and PATCH …/{id} take; every field optional on a PATCH except that slug is refused there.
Attributes
namestringslugstring- Create only: lowercase letters, digits and hyphens, up to 40; derived from the name when blank.
descriptionstringshow_promo_fieldbooleanshow_bank_transferbooleanshow_express_checkoutbooleanshow_quote_buttonbooleanorganizations_onlybooleancountriesarray of string- ISO 3166-1 alpha-2 codes, any case; replaces the whole list when sent, and [] allows any country. An unknown code is refused with
422 invalid_body.
{
"name": "Press",
"slug": "press",
"description": "For accredited press.",
"max_per_order": 2,
"show_promo_field": true,
"show_bank_transfer": true,
"show_express_checkout": true,
"show_quote_button": false,
"ref_prefix": "PR",
"organizations_only": false,
"ask_vat": true,
"countries": [
"IE"
],
"ticket_type_ids": [
8
]
}
The PortalType object
One ticket type in a portal's scope.
Attributes
{
"id": 8,
"allocation": 0
}
The Portal object
A sponsor's or partner's own page with an allocation of tickets to hand out.
Attributes
slugstring- The readable share-address segment, with its random tail. A PATCH with different words moves it to a new tail, and regenerate gives it a new tail.
namestringper_typeboolean- Whether the allocation is a set number of EACH type (the sum of
ticket_types[].allocation) rather than a pool. package_due_datestring or null- The day the fee is due, on the event's clock; null for "before they can hand anything out".
package_tax_rate_idinteger- The tax rate the fee is charged at, from the account's rates; 0 for no VAT.
package_price_includes_taxboolean- Whether package_cents already includes the VAT, or the VAT is added on top. Always false with no rate.
capabilitiesarray of string- The What it does set switched on; empty is plain tickets. Sections: offerings, details, documents, downloads, guests. The holder's own ticket controls: change_type, buy_more, team. names_only means the holder gives a name and an email when assigning a ticket, and the attendee answers the event's questions.
access_lockedboolean- Whether the holder must sign in with an address the access list admits. The list itself is not reported.
give_out_untilstring (RFC 3339) or null- The portal's cut-off: after this moment the holder can no longer send links, assign tickets, upload a list or register people on the share link. Tickets already given out, and links already sent, keep working. null for no cut-off.
archivedbooleanrevokedbooleancreated_atstring (RFC 3339) or null
{
"id": 3,
"ref": "P7K2QM",
"slug": "amgen-k8wa6",
"name": "Amgen",
"url": "https://acct.go.tito.io/annual/portals/1qR20rFnPEV70bqeGNLJZg",
"share_url": null,
"shareable": false,
"share_ticket_type_ids": [
12,
14
],
"contact_email": "sponsor@example.com",
"currency": "eur",
"allocation": 100,
"per_type": false,
"ticket_types": [
{
"allocation": 0,
"id": 8
}
],
"issued": 12,
"pending": 3,
"remaining": 85,
"ref_prefix": "SPN",
"package": "Gold stand",
"package_cents": 250000,
"package_detail": "Two passes and a stand",
"package_due_date": "2030-03-01",
"package_tax_rate_id": 0,
"package_price_includes_tax": false,
"capabilities": [
"offerings"
],
"access_locked": false,
"give_out_until": null,
"archived": false,
"revoked": false,
"sent_at": null,
"created_at": "2026-09-07T22:00:00Z"
}
The PortalInput object
What POST /events/{slug}/portals and PATCH …/{id} take. On a POST the name and one scope shape are required; on a PATCH every field is optional and currency, ticket_type_ids and ticket_types are refused.
Attributes
namestringslugstring- The shareable link's readable words: lowercase letters, digits and hyphens, up to 60; derived from the name when blank. A random tail is always appended, so the answer's
slugends in five more characters. On a PATCH, different words move the address and the old one stops working. ticket_typesarray of PortalType- Create only: a per-type portal's scope, each type with its own number (1–1000000).
packagestringpackage_detailstringpackage_tax_rate_idinteger- A tax rate's id, or 0 for no VAT. Ignored (stored as 0) when the portal charges no fee. Refused with package_billed while a bill stands.
package_price_includes_taxboolean- Whether package_cents already includes the VAT; false adds it on top. Refused with package_billed while a bill stands.
access_lockedboolean- Switch the sign-in lock; needs at least one access_list entry. A portal whose team capability is on stays locked, and unlocking it is refused with team_locked.
access_listarray of string- Addresses and domains the lock admits; replaces the whole list when sent.
give_out_untilstring- The cut-off as an RFC 3339 moment, or "" for none. Refused with invalid_dates when it will not read.
{
"name": "Amgen",
"slug": "amgen",
"currency": "eur",
"allocation": 100,
"ticket_type_ids": [
8
],
"ticket_types": [
{
"allocation": 10,
"id": 8
}
],
"contact_email": "sponsor@example.com",
"ref_prefix": "SPN",
"package": "Gold stand",
"package_cents": 250000,
"package_detail": "Two passes and a stand",
"package_due_date": "2030-03-01",
"package_tax_rate_id": 0,
"package_price_includes_tax": false,
"access_locked": false,
"access_list": [
"amgen.com",
"boss@example.com"
],
"give_out_until": "2030-03-01T17:30:00Z",
"shareable": true,
"share_ticket_type_ids": [
12
]
}
The PortalSendInput object
What POST …/{id}/send takes; every field optional, and no body at all sends the stock email to the contact address.
Attributes
{
"to": "sponsor@example.com",
"subject": "Your Amgen portal",
"body": "Here is your portal for the conference."
}
The ApplicationPage object
A public form people fill in to ask for a place.
Attributes
{
"id": 2,
"slug": "students",
"url": "https://acct.go.tito.io/annual/request/students",
"name": "Student places",
"public_heading": "Ask for a student place",
"public_description": "Places are for full-time students.\n\nBring your student card on the day.",
"decide_by": "review",
"pay_mode": "invite",
"ticket_type_ids": [
8
],
"max_quantity": 4,
"closes_at": null,
"closed": false,
"reject_email": true,
"reject_message": "Sorry, not this time.",
"draw_places": 0,
"target_count": 0,
"waiting": 3,
"applied": 12,
"approved": 7,
"not_accepted": 1,
"withdrawn": 1,
"ended": false,
"created_at": "2026-09-07T22:40:00Z"
}
The ApplicationPageInput object
What POST /events/{slug}/application-pages and PATCH …/{id} take; on a POST the name and ticket_type_ids are required, on a PATCH every field is optional.
Attributes
{
"name": "Student places",
"slug": "students",
"public_heading": "Ask for a student place",
"public_description": "Places are for full-time students.\n\nBring your student card on the day.",
"decide_by": "review",
"pay_mode": "invite",
"ticket_type_ids": [
8
],
"max_quantity": 4,
"closes_at": "2030-01-01T09:00:00Z",
"reject_email": true,
"reject_message": "Sorry, not this time.",
"draw_places": 10,
"target_count": 50
}
The WaitlistEntry object
One person on a ticket type's or a workshop option's waitlist.
Attributes
idintegernamestringemailstringticket_type_idinteger or null- The ticket type whose waitlist they are on. null when they are waiting for a workshop option.
ticket_type_namestring or null- That ticket type's name. null when they are waiting for a workshop option.
session_idinteger or null- The workshop option whose waitlist they are on. null when they are waiting for a ticket type.
session_namestring or null- That option's name, or "" for an option with no name of its own. null when they are waiting for a ticket type.
offering_idinteger or null- The workshop offering the option belongs to. null when they are waiting for a ticket type.
ticket_refstring or null- The reference of the ticket that is waiting for the workshop option. null once the ticket goes to someone else or its workshop place is refunded, and when they are waiting for a ticket type.
statestring- waiting, offered (emailed about a free place, or offered one by hand, see held_until), took or left.
- one of
waitingofferedtookleft sourcestring- page when they joined from the event page, organizer when someone added them by hand, ticket when they joined a workshop option's waitlist from their ticket.
- one of
pageorganizerticket joined_atstring (RFC 3339)positioninteger or null- Their place in the queue for this ticket type or workshop option, counting only people waiting. null unless they are waiting. For a workshop option, also null once the ticket goes to someone else or its workshop place is refunded.
offered_atstring (RFC 3339) or null- When they were last emailed about a free place. null if they never have been.
held_untilstring (RFC 3339) or null- When the place offered to them by hand stops being held for them. null unless they have been offered a place by hand.
anonymized_atstring (RFC 3339) or null- When the person on this entry was anonymized, null while they are still on it. An anonymized entry keeps its reference, its list and its state and has no name or email address.
{
"id": 12,
"ref": "V6EWNE",
"name": "Aoife Byrne",
"email": "aoife@example.com",
"ticket_type_id": 8,
"ticket_type_name": "Early Bird",
"session_id": null,
"session_name": null,
"offering_id": null,
"offering_name": null,
"ticket_ref": null,
"state": "waiting",
"source": "page",
"added_by": "",
"joined_at": "2026-09-23T10:15:00Z",
"left_at": null,
"position": 1,
"locale": "en",
"offered_at": null,
"took_at": null,
"order_id": null,
"order_ref": null,
"held_until": null,
"offered_by": "",
"anonymized_at": "2026-10-14T09:00:00Z"
}
The WaitlistEntryDetail object
One person on a waitlist, with their answers.
Attributes
idintegernamestringemailstringticket_type_idinteger or null- The ticket type whose waitlist they are on. null when they are waiting for a workshop option.
ticket_type_namestring or null- That ticket type's name. null when they are waiting for a workshop option.
session_idinteger or null- The workshop option whose waitlist they are on. null when they are waiting for a ticket type.
session_namestring or null- That option's name, or "" for an option with no name of its own. null when they are waiting for a ticket type.
offering_idinteger or null- The workshop offering the option belongs to. null when they are waiting for a ticket type.
ticket_refstring or null- The reference of the ticket that is waiting for the workshop option. null once the ticket goes to someone else or its workshop place is refunded, and when they are waiting for a ticket type.
statestring- waiting, offered (emailed about a free place, or offered one by hand, see held_until), took or left.
- one of
waitingofferedtookleft sourcestring- page when they joined from the event page, organizer when someone added them by hand, ticket when they joined a workshop option's waitlist from their ticket.
- one of
pageorganizerticket joined_atstring (RFC 3339)positioninteger or null- Their place in the queue for this ticket type or workshop option, counting only people waiting. null unless they are waiting. For a workshop option, also null once the ticket goes to someone else or its workshop place is refunded.
offered_atstring (RFC 3339) or null- When they were last emailed about a free place. null if they never have been.
held_untilstring (RFC 3339) or null- When the place offered to them by hand stops being held for them. null unless they have been offered a place by hand.
answersarray of WaitlistAnswer- Their answers to the waitlist's questions. Blank answers are left out. Always empty for a workshop option's waitlist, which asks none.
answers[].field_idintegeranswers[].labelstringanswers[].valuestringanonymized_atstring (RFC 3339) or null- When the person on this entry was anonymized, null while they are still on it. An anonymized entry keeps its reference, its list and its state and has no name or email address.
{
"id": 12,
"ref": "V6EWNE",
"name": "Aoife Byrne",
"email": "aoife@example.com",
"ticket_type_id": 8,
"ticket_type_name": "Early Bird",
"session_id": null,
"session_name": null,
"offering_id": null,
"offering_name": null,
"ticket_ref": null,
"state": "waiting",
"source": "page",
"added_by": "",
"joined_at": "2026-09-23T10:15:00Z",
"left_at": null,
"position": 1,
"locale": "en",
"offered_at": null,
"took_at": null,
"order_id": null,
"order_ref": null,
"held_until": null,
"offered_by": "",
"answers": [
{
"field_id": 31,
"key": "dietary_needs",
"label": "Dietary needs",
"value": "Vegan"
}
],
"anonymized_at": "2026-10-14T09:00:00Z"
}
The WaitlistAnswer object
One answer to a waitlist question, as it was given.
Attributes
{
"field_id": 31,
"key": "dietary_needs",
"label": "Dietary needs",
"value": "Vegan"
}
The WaitlistCounts object
How many people are in each state.
Attributes
{
"waiting": 14,
"offered": 0,
"took": 0,
"left": 2
}
The WaitlistOfferInput object
Who to offer a place to, how long each place is held, and optionally the email's words.
Attributes
idsarray of integer- The people to offer a place to, by their waitlist id. All waiting for the same ticket type.
ticket_type_idinteger- With
next, instead ofids: offer a place to the next people waiting for this ticket type. subjectstring- The email's subject. Give it with
message, or neither for the usual words in each person's own language. messagestring- The offer's words. Blank lines separate paragraphs. When the hold ends, the button and the leave line are added under it.
{
"ids": [
41,
42
],
"ticket_type_id": 8,
"next": 2,
"hold_hours": 48,
"subject": "A Student place is held for you at Wander 2026",
"message": "A place on Student at Wander 2026 has become free, and we’re keeping it for you."
}
The WaitlistOfferResult object
What an offer did.
Attributes
{
"offered": [
41,
42
],
"skipped": [
{
"id": 43,
"reason": "already_held"
}
],
"held_until": "2026-09-26T09:15:00Z",
"free_before": 1,
"over_by": 1
}
The WaitlistAddInput object
Someone to add to a waitlist.
Attributes
namestringemailstringanswersobject- Answers keyed by question id. Optional. Each answer must be one the question can hold. A yes/no question takes "yes" or "no". A choice takes one of its options, and a multiple choice takes its options separated by ", ". A phone number is written in international form and stored that way. A number must be a number and a date must be YYYY-MM-DD. A country question takes a two-letter country code ("IE"), one its list offers; a country name is refused. Blank answers are left out.
{
"name": "Niamh Reilly",
"email": "niamh@example.com",
"ticket_type_id": 8,
"answers": {
"31": "Vegan"
},
"notify": true
}
The Application object
One person's request for a place.
Attributes
idintegernamestringemailstringstatestring- new, approved, rejected, withdrawn, given (a place marked by hand before a draw runs) or ruledout.
- one of
newapprovedrejectedwithdrawngivenruledout asked_ticket_type_idinteger or nullasked_quantityintegerinvitation_idinteger or null- The invitation an approval minted; null on a charged approval or before one.
decided_atstring (RFC 3339) or nullcard_brandstringcard_last4stringanswers[].field_idintegeranswers[].valuestringcreated_atstring (RFC 3339) or nullanonymized_atstring (RFC 3339) or null- When the requester was anonymized, null while they are still on it. An anonymized request keeps its reference, what was asked for and its state, and has no name or email address.
{
"id": 41,
"page_id": 2,
"ref": "R7K2QM",
"name": "Aoife Ní Bhriain",
"email": "aoife@example.com",
"locale": "ga",
"state": "new",
"asked_ticket_type_id": 8,
"asked_quantity": 3,
"granted_ticket_type_id": null,
"granted_quantity": 0,
"invitation_id": null,
"order_ref": null,
"decided_at": null,
"decided_by": "",
"card_state": "",
"card_brand": "",
"card_last4": "",
"charge_failure": "",
"answers": [
{
"field_id": 5,
"value": "UCD"
}
],
"created_at": "2026-09-07T22:41:00Z",
"anonymized_at": "2026-10-14T09:00:00Z"
}
The ApplicationApproveInput object
What POST …/approve takes; both optional, and no body grants exactly what was asked.
Attributes
{
"ticket_type_id": 8,
"quantity": 2
}
The ApplicationRejectInput object
What POST …/reject takes; optional, and no body follows the page's own switch.
Attributes
emailboolean- false skips this one person's email on a page that sends them; true is refused on a page that does not.
{
"email": false
}
The QuoteLine object
One priced line on a quote, as it was when the quote was made.
Attributes
ticket_type_idinteger or nullproduct_idinteger or nullnamestringquantityintegerunit_price_centsintegertax_centsintegertax_namestringtaxesarray of object- Each tax rate the line is quoted at, with its own share of
tax_cents: one entry for one rate, two for two. Absent on a line with no tax.
{
"kind": "ticket",
"ticket_type_id": 8,
"product_id": null,
"name": "VIP",
"quantity": 2,
"charge_quantity": 2,
"unit_price_cents": 10000,
"tax_cents": 0,
"tax_name": "",
"taxes": [
{
"name": "GST",
"rate_milli_pct": 5000,
"tax_cents": 250
}
]
}
The Quote object
A priced selection sent to a buyer.
Attributes
idintegeremailstringnamestringorgstringstatusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
- one of
opensubmittedrejectedimportedexpiredcanceled currencystringpromo_discount_centsintegerpromo_codesarray of object- Every code the quote was priced with, in the order the buyer added them, with what each takes off. promo_code is the first and promo_discount_cents the total.
promo_codes[].codestringpromo_codes[].discount_centsintegerlinesarray of QuoteLinelines[].ticket_type_idinteger or nulllines[].product_idinteger or nulllines[].namestringlines[].quantityintegerlines[].charge_quantityinteger- How many of quantity are paid for; a group discount zeroes the free ones.
lines[].unit_price_centsintegerlines[].tax_centsintegerlines[].tax_namestringlines[].taxesarray of object- Each tax rate the line is quoted at, with its own share of
tax_cents: one entry for one rate, two for two. Absent on a line with no tax. expires_atstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nullneeds_approvalboolean- An open quote whose buyer asks for approval. The buyer's page sends it to an approver instead of converting it.
approval.approver.refstringapproval.approver.namestringapproval.submitted_atstring (RFC 3339) or nullapproval.rejected_atstring (RFC 3339) or nullapproval.po_numberstring or null- The purchase order that approved the quote, null until one is recorded. Recording it converts the quote.
approval.recorded_bystring or null- The approver who recorded the purchase order, null when the organizer recorded it or none is recorded.
approval.recorded_doorstring or null- Who recorded it:
approver(an approver on the buyer it was sent to),buyer(an approver on the quote's own buyer) ororganizer. Null until recorded. - one of
approverbuyerorganizer<nil> approval.recorded_atstring (RFC 3339) or nullbuyerobject or null- The buyer the quote is filed under, null for none: a quote made by somebody signed in, or composed for an address that is one of exactly one buyer's people. That buyer's approval applies to it.
buyer.refstringbuyer.namestringwarningobject- Only on a create's answer, and only when the quote could not be filed under a buyer it might belong to:
several_buyerswhen the email is one of the people on more than one buyer. warning.codestringwarning.messagestring
{
"id": 12,
"ref": "2QY8FN",
"url": "https://acct.go.tito.io/annual/quotes/1qR20rFnPEV70bqeGNLJZg",
"email": "accounts@example.com",
"name": "Mary Byrne",
"org": "St Mary's",
"status": "open",
"amount_cents": 20000,
"currency": "eur",
"promo_code": "",
"promo_discount_cents": 0,
"promo_codes": [
{
"code": "SCHOOL10",
"discount_cents": 500
}
],
"locale": "en",
"lines": [
{
"charge_quantity": 2,
"kind": "ticket",
"name": "VIP",
"product_id": null,
"quantity": 2,
"tax_cents": 0,
"tax_name": "",
"ticket_type_id": 8,
"unit_price_cents": 10000
}
],
"order_ref": null,
"expires_at": "2026-09-21T22:50:00Z",
"created_at": "2026-09-07T22:50:00Z",
"needs_approval": false,
"approval": {
"approver": {
"name": "Example Ltd",
"ref": "k3m8p1"
},
"po_number": null,
"recorded_at": null,
"recorded_by": null,
"recorded_door": null,
"rejected_at": null,
"rejected_by": null,
"rejected_reason": null,
"submitted_at": "2026-09-08T09:15:00Z",
"submitted_by": "alex@example.com"
},
"buyer": {
"name": "Example Ltd Design",
"ref": "p2q7r4"
},
"warning": {
"code": "several_buyers",
"message": "example"
}
}
The QuoteItemInput object
One line to quote: a ticket type or an extra.
Attributes
{
"ticket_type_id": 8,
"product_id": 3,
"quantity": 2,
"unit_price_cents": 9000
}
The QuoteInput object
What POST /events/{slug}/quotes takes.
Attributes
{
"email": "accounts@example.com",
"name": "Mary Byrne",
"org": "St Mary's",
"currency": "eur",
"items": [
{
"quantity": 2,
"ticket_type_id": 8
}
],
"send": true
}
The QuoteConvertInput object
What POST …/convert takes; optional.
Attributes
confirmboolean- Convert at the new total when the quote's total moved, with
confirm_total_centsset to that total. po_numberstring- A purchase order number the quote's approver sent you. Only on a submitted quote: recording it approves and converts the quote, and puts the number on the invoice issued to the quote's buyer.
confirm_total_centsinteger- The new total
confirmis for: thenow_centsof the409 repricedanswer.confirmwithout it confirms nothing. A confirmation counts only while the total is still that figure, so a price that moves again is refused again.
{
"confirm": true,
"po_number": "PO-4471",
"confirm_total_cents": 5000
}
The EventRegistrationSettings object
The sales panel: when selling stops, when the questions get asked, and quotes.
Attributes
sales_end_modestring- "ends" (when the event ends), "never", or "custom" with sales_end_at saying when.
- one of
endsnevercustom field_collection_timingstring- Whether the questions are asked before paying or after.
- one of
before_paymentafter_payment require_holder_namebooleanrequire_holder_emailbooleanrequire_holder_answersboolean- Whether a ticket the booker gives to somebody else must have its required questions answered before it is sent.
qr_gatestring- When a ticket's QR code appears: always, once it is named, or once its questions are done.
- one of
alwaysnameddone min_tickets_per_orderinteger- The fewest tickets one buyer's order may hold, 0 for no limit. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it.
max_tickets_per_orderinteger- The most tickets one buyer's order may hold, 0 for no limit. Counted as min_tickets_per_order is, and an addition to an order counts what the order already holds.
capacityinteger or null- The most tickets the whole event can sell, across every ticket type, null for no limit. Once the event reaches it every ticket type is off sale, whatever each has left. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it. An organizer's own orders in the admin may go past it.
{
"sales_end_mode": "ends",
"sales_end_at": null,
"field_collection_timing": "before_payment",
"require_holder_name": false,
"require_holder_email": false,
"require_holder_answers": false,
"qr_gate": "always",
"quotes_enabled": false,
"quotes_on_main": false,
"min_tickets_per_order": 0,
"max_tickets_per_order": 6,
"capacity": 500,
"quote_audience_ids": [
6
]
}
The EventRegistrationSettingsInput object
What PATCH …/settings/registration takes; every field optional.
Attributes
sales_end_modestring- "ends" (when the event ends), "never", or "custom" with sales_end_at saying when.
- one of
endsnevercustom field_collection_timingstring- Whether the questions are asked before paying or after.
- one of
before_paymentafter_payment require_holder_namebooleanrequire_holder_emailbooleanrequire_holder_answersboolean- Whether a ticket the booker gives to somebody else must have its required questions answered before it is sent.
qr_gatestring- When a ticket's QR code appears: always, once it is named, or once its questions are done.
- one of
alwaysnameddone min_tickets_per_orderinteger- The fewest tickets one buyer's order may hold, 0 for no limit. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it.
max_tickets_per_orderinteger- The most tickets one buyer's order may hold, 0 for no limit. Counted as min_tickets_per_order is, and an addition to an order counts what the order already holds.
capacityinteger or null- The most tickets the whole event can sell, across every ticket type, null for no limit. Once the event reaches it every ticket type is off sale, whatever each has left. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it. An organizer's own orders in the admin may go past it.
{
"sales_end_mode": "ends",
"sales_end_at": null,
"field_collection_timing": "before_payment",
"require_holder_name": false,
"require_holder_email": false,
"require_holder_answers": false,
"qr_gate": "always",
"quotes_enabled": false,
"quotes_on_main": false,
"min_tickets_per_order": 0,
"max_tickets_per_order": 6,
"capacity": 500,
"quote_audience_ids": [
6
]
}
The EventPaymentSettings object
The payment panel: how the event takes money. Requires payment.manage, which owners and admins hold.
Attributes
stripe_accountstring- Which of the account's Stripe accounts this event's new card payments go into: an account id, empty for the account's default, or
nonewhen nobody has picked one and the event takes no card payments. Orders already paid stay with the account they were paid into. checkout_modestring- Where buyers pay: hosted (Stripe's own page, the zero-JS baseline) or custom (on the event's own pages, with Stripe's payment fields). Events saved with the retired value embedded read back as custom.
- one of
hostedcustom hold_minutesinteger- How many minutes a buyer's selection is held for them while they fill in their details, 5 to 60. A hold is extended while nobody else is waiting for the tickets.
stripe_customers_enabledbooleanbank_transfer_enabledbooleanbank_transfer_reminder_daysstring- Days before the due date to remind, comma-separated. Normalized on write.
bank_transfer_overdue_daysstring- Days after the due date to chase, comma-separated. Normalized on write.
bank_transfer_auto_cancel_daysinteger- Days overdue before the order is released; 0 is off, 365 the ceiling.
paypal_enabledbooleanexpress_checkout_enabledboolean- Whether buyers are offered Apple Pay, Google Pay, Link and PayPal as one-tap shortcuts. Card payment stays either way, and PayPal stays where it is the only way to pay. Off here wins over an audience's own
show_express_checkout. extra_currenciesarray of string- Currencies this event sells in besides its own, in the account's order.
{
"payment_expiry_days": 0,
"stripe_account": "acct_1MEXAMPLE",
"checkout_mode": "custom",
"hold_minutes": 15,
"stripe_customers_enabled": false,
"bank_transfer_enabled": false,
"bank_transfer_due_days": 14,
"bank_transfer_reminder_days": "1,3,7",
"bank_transfer_overdue_days": "1,7",
"bank_transfer_auto_cancel_days": 0,
"paypal_enabled": false,
"express_checkout_enabled": true,
"extra_currencies": [
"usd",
"gbp"
],
"currency_assignment": ""
}
The EventPaymentSettingsInput object
What PATCH …/settings/payment takes; every field optional.
Attributes
stripe_accountstring- One of the account's Stripe accounts (see the Stripe connection's
stripe_accounts). Anything else is refused. Changing it moves only the event's new card payments. checkout_modestring- Where buyers pay: hosted (Stripe's own page, the zero-JS baseline) or custom (on the event's own pages, with Stripe's payment fields). embedded is a retired alias, still accepted and stored as custom.
- one of
hostedcustomembedded hold_minutesinteger- How many minutes a buyer's selection is held for them while they fill in their details, 5 to 60. A hold is extended while nobody else is waiting for the tickets.
stripe_customers_enabledbooleanbank_transfer_enabledbooleanbank_transfer_reminder_daysstring- Days before the due date to remind, comma-separated. Normalized on write.
bank_transfer_overdue_daysstring- Days after the due date to chase, comma-separated. Normalized on write.
bank_transfer_auto_cancel_daysinteger- Days overdue before the order is released; 0 is off, 365 the ceiling.
paypal_enabledbooleanexpress_checkout_enabledboolean- Whether buyers are offered Apple Pay, Google Pay, Link and PayPal as one-tap shortcuts. Card payment stays either way, and PayPal stays where it is the only way to pay. Off here wins over an audience's own
show_express_checkout. extra_currenciesarray of string- Currencies this event sells in besides its own, in the account's order.
{
"payment_expiry_days": 0,
"stripe_account": "acct_1MEXAMPLE",
"checkout_mode": "custom",
"hold_minutes": 15,
"stripe_customers_enabled": false,
"bank_transfer_enabled": false,
"bank_transfer_due_days": 14,
"bank_transfer_reminder_days": "1,3,7",
"bank_transfer_overdue_days": "1,7",
"bank_transfer_auto_cancel_days": 0,
"paypal_enabled": false,
"express_checkout_enabled": true,
"extra_currencies": [
"usd",
"gbp"
],
"currency_assignment": ""
}
The EventCheckoutSettings object
An event's Checkout panel: its own answer to the account's price display and company question, and the link to its terms and conditions.
Attributes
tax_displaystring- This event's own answer:
incshows prices including tax,excexcluding it,inheritfollows the account. - one of
inheritincexc tax_display_excludesboolean- The resolved answer, and the one to read: true when this event's public prices are shown excluding tax.
ask_companystring- This event's own answer to whether the checkout asks if the buyer is booking for a company.
onlets the buyer tick a box,organizationsmakes every booking an organization's with no box to tick,offnever asks, andinheritfollows the account. - one of
inheritonofforganizations company_fieldsobject- What an
organizationsevent does with each of an organization's details beyond its name, which is always asked and required:hiddenleaves it off the form,optionalasks for it,requiredrefuses the booking without it. Kept but not applied under any otherask_company. On PATCH, the details named are set and the rest keep their state. terms_urlstring- The organizer's own terms and conditions page, or an empty string for none. Set, the booking form carries a line above the button at its end saying buyers agree to these terms, linked to this address.
collect_unfinishedboolean- Collect emails from people who don't finish registering. On, the booking form says "We may email you about this event" above its payment button. A buyer who stops after reading it is listed under Abandoned in Orders and can be messaged until the event ends. A buyer who stopped while it was off is never listed. Off by default.
{
"tax_display": "inherit",
"tax_display_excludes": false,
"ask_company": "inherit",
"company_fields": {
"address": "optional",
"country": "required",
"po": "hidden",
"vat": "required"
},
"asks_company": true,
"terms_url": "https://example.com/terms",
"collect_unfinished": false
}
The EventCheckoutSettingsInput object
What PATCH …/settings/checkout takes; every field optional.
Attributes
tax_displaystring- This event's own answer:
incshows prices including tax,excexcluding it,inheritfollows the account. - one of
inheritincexc ask_companystring- This event's own answer to whether the checkout asks if the buyer is booking for a company.
onlets the buyer tick a box,organizationsmakes every booking an organization's with no box to tick,offnever asks, andinheritfollows the account. - one of
inheritonofforganizations company_fieldsobject- What an
organizationsevent does with each of an organization's details beyond its name, which is always asked and required:hiddenleaves it off the form,optionalasks for it,requiredrefuses the booking without it. Kept but not applied under any otherask_company. On PATCH, the details named are set and the rest keep their state. terms_urlstring- An http or https address, or an empty string to take the link off. A bare domain such as
example.com/termsis read as https. Anything else is refused. collect_unfinishedboolean- Switch collecting emails from people who don't finish registering on or off. Switching it off stops collecting anyone new. People already collected stay until the event ends.
{
"tax_display": "inherit",
"ask_company": "inherit",
"company_fields": {
"address": "optional",
"country": "required",
"po": "hidden",
"vat": "required"
},
"terms_url": "https://example.com/terms",
"collect_unfinished": true
}
The EventAfterSettings object
The after-purchase panel: what a buyer may change once they have paid.
Attributes
email_sender_idinteger- The account sender (
GET /email-senders) — the domain — this event's email goes out from, or 0 to follow the account's default. email_sender_address_idinteger- The address on that sender's domain this event's email goes out as (an
idfrom the sender'saddresses), or 0 to follow the account's default. reply_tostring- Where replies to this event's email go. It wins over the Reply-To of the address the email goes out as, and works with no sending domain at all. "" follows the address: its own Reply-To, or the address itself.
receipt_deliverystring- How the order confirmation hands over a paid order's receipt: attached as a PDF, as a link to it, or not at all (the buyer downloads it from their order page).
- one of
attachlinknone invoice_details_daysinteger- How many days after each order its buyer may update their own invoice details from their order page, or 0. At most one of this and
invoice_details_untilis set; neither means buyers can't. invoice_details_untilstring- Until when buyers may update their own invoice details,
2006-01-02T15:04on the event's clock, or "". holder_changes_messagestring- What an attendee reads when the window is shut; "" means the shipped sentence, in their own language.
holder_email_lockedbooleanholder_details_lockedbooleanself_service_changesboolean- Whether a buyer may remove a ticket or cancel from their own order page. Off by default: it moves money.
calendar_links_enabledboolean- Whether people are offered Add to calendar after they book: on their order page, their ticket page and in the emails. On by default. The offer on the event page before booking is a separate switch,
calendar_on_event_pageon the event. allow_upgradesboolean- Whether buyers can move a ticket to a dearer ticket type from their order page. An upgrade costs the difference between the two ticket prices, whatever the buyer paid. On by default.
allow_downgradesboolean- Whether buyers can move a ticket to a cheaper ticket type from their order page, and get the difference back. Nobody gets back more than they paid for their ticket. Off by default.
{
"email_sender_id": 0,
"email_sender_address_id": 0,
"reply_to": "",
"receipt_delivery": "attach",
"invoice_details_days": 30,
"invoice_details_until": "",
"holder_changes_stopped": false,
"holder_changes_until": "",
"holder_changes_message": "",
"holder_email_locked": false,
"holder_details_locked": false,
"self_service_changes": false,
"self_service_until": "",
"order_qr_code_enabled": false,
"calendar_links_enabled": true,
"allow_upgrades": true,
"allow_downgrades": false
}
The EventAfterSettingsInput object
What PATCH …/settings/after-registering takes; every field optional.
Attributes
email_sender_idinteger- The account sender this event's email goes out from, or 0 to follow the account's default. Must be 0, the event's current sender, or one of the account's
livesenders. Given alone, the event sends as that sender's primary address (itsfrom_local). Given withemail_sender_address_id, it must be that address's sender. email_sender_address_idinteger- The address this event's email goes out as, or 0 to follow the account's default. Must be 0, the event's current address, or an address on one of the account's
livesenders. reply_tostring- Where replies to this event's email go: one plain email address, or "" to follow the address the email goes out as. Anything else is HTTP 422
invalid_body. receipt_deliverystring- How the order confirmation hands over a paid order's receipt: attached as a PDF, as a link to it, or not at all (the buyer downloads it from their order page).
- one of
attachlinknone invoice_details_daysinteger- How many days after each order its buyer may update their own invoice details from their order page, or 0. At most one of this and
invoice_details_untilis set; neither means buyers can't. Setting it above 0 replaces a date window; 0 turns a days window off. invoice_details_untilstring- Until when buyers may update their own invoice details,
2006-01-02T15:04on the event's clock, or "". Setting it replaces a days window; "" turns a date window off. Sending it withinvoice_details_daysabove 0 answers 422. holder_changes_messagestring- What an attendee reads when the window is shut; "" means the shipped sentence, in their own language.
holder_email_lockedbooleanholder_details_lockedbooleanself_service_changesboolean- Whether a buyer may remove a ticket or cancel from their own order page. Off by default: it moves money.
calendar_links_enabledboolean- Whether people are offered Add to calendar after they book: on their order page, their ticket page and in the emails. On by default. The offer on the event page before booking is a separate switch,
calendar_on_event_pageon the event. allow_upgradesboolean- Whether buyers can move a ticket to a dearer ticket type from their order page. An upgrade costs the difference between the two ticket prices, whatever the buyer paid. On by default.
allow_downgradesboolean- Whether buyers can move a ticket to a cheaper ticket type from their order page, and get the difference back. Nobody gets back more than they paid for their ticket. Off by default.
{
"email_sender_id": 0,
"email_sender_address_id": 0,
"reply_to": "help@example.com",
"receipt_delivery": "attach",
"invoice_details_days": 30,
"invoice_details_until": "",
"holder_changes_stopped": false,
"holder_changes_until": "",
"holder_changes_message": "",
"holder_email_locked": false,
"holder_details_locked": false,
"self_service_changes": false,
"self_service_until": "",
"order_qr_code_enabled": false,
"calendar_links_enabled": true,
"allow_upgrades": true,
"allow_downgrades": false
}
The EventFeatureSettings object
Which sections this event's admin sidebar shows. A preference, never a kill switch: it hides nothing from a buyer and stops no route.
Attributes
in_usearray of string- Sections something is already using; these show no switch and carry no preference.
{
"sections": [
"quotes"
],
"available": [
"quotes",
"orders"
],
"in_use": [
"orders"
]
}
The EventFeatureSettingsInput object
What PATCH …/settings/features takes.
Attributes
{
"sections": [
"quotes"
]
}
The EventAutomatedEmailSwitches object
Which of an event's automated emails are switched off.
Attributes
{
"switched_off": [
"order"
],
"switchable": [
"moved",
"order",
"order_changed",
"request",
"rsvp",
"ticket",
"waitlist_already",
"waitlist_joined"
]
}
The EventAutomatedEmailSwitchesInput object
What PATCH …/settings/automated-emails takes.
Attributes
{
"switched_off": [
"order"
]
}
The EventSettings object
Every settings panel this API serves, in one read.
Attributes
registration.sales_end_modestring- "ends" (when the event ends), "never", or "custom" with sales_end_at saying when.
- one of
endsnevercustom registration.sales_end_atstring (RFC 3339) or null- When selling stops; null unless the mode is custom.
registration.field_collection_timingstring- Whether the questions are asked before paying or after.
- one of
before_paymentafter_payment registration.require_holder_namebooleanregistration.require_holder_emailbooleanregistration.require_holder_answersboolean- Whether a ticket the booker gives to somebody else must have its required questions answered before it is sent.
registration.qr_gatestring- When a ticket's QR code appears: always, once it is named, or once its questions are done.
- one of
alwaysnameddone registration.min_tickets_per_orderinteger- The fewest tickets one buyer's order may hold, 0 for no limit. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it.
registration.max_tickets_per_orderinteger- The most tickets one buyer's order may hold, 0 for no limit. Counted as min_tickets_per_order is, and an addition to an order counts what the order already holds.
registration.capacityinteger or null- The most tickets the whole event can sell, across every ticket type, null for no limit. Once the event reaches it every ticket type is off sale, whatever each has left. Tickets only: items and add-ons don't count, and a bundle counts as the tickets inside it. An organizer's own orders in the admin may go past it.
payment.stripe_accountstring- Which of the account's Stripe accounts this event's new card payments go into: an account id, empty for the account's default, or
nonewhen nobody has picked one and the event takes no card payments. Orders already paid stay with the account they were paid into. payment.checkout_modestring- Where buyers pay: hosted (Stripe's own page, the zero-JS baseline) or custom (on the event's own pages, with Stripe's payment fields). Events saved with the retired value embedded read back as custom.
- one of
hostedcustom payment.hold_minutesinteger- How many minutes a buyer's selection is held for them while they fill in their details, 5 to 60. A hold is extended while nobody else is waiting for the tickets.
payment.stripe_customers_enabledbooleanpayment.bank_transfer_enabledbooleanpayment.bank_transfer_reminder_daysstring- Days before the due date to remind, comma-separated. Normalized on write.
payment.bank_transfer_overdue_daysstring- Days after the due date to chase, comma-separated. Normalized on write.
payment.bank_transfer_auto_cancel_daysinteger- Days overdue before the order is released; 0 is off, 365 the ceiling.
payment.paypal_enabledbooleanpayment.express_checkout_enabledboolean- Whether buyers are offered Apple Pay, Google Pay, Link and PayPal as one-tap shortcuts. Card payment stays either way, and PayPal stays where it is the only way to pay. Off here wins over an audience's own
show_express_checkout. payment.extra_currenciesarray of string- Currencies this event sells in besides its own, in the account's order.
after_registering.email_sender_idinteger- The account sender (
GET /email-senders) — the domain — this event's email goes out from, or 0 to follow the account's default. after_registering.email_sender_address_idinteger- The address on that sender's domain this event's email goes out as (an
idfrom the sender'saddresses), or 0 to follow the account's default. after_registering.reply_tostring- Where replies to this event's email go. It wins over the Reply-To of the address the email goes out as, and works with no sending domain at all. "" follows the address: its own Reply-To, or the address itself.
after_registering.receipt_deliverystring- How the order confirmation hands over a paid order's receipt: attached as a PDF, as a link to it, or not at all (the buyer downloads it from their order page).
- one of
attachlinknone after_registering.invoice_details_daysinteger- How many days after each order its buyer may update their own invoice details from their order page, or 0. At most one of this and
invoice_details_untilis set; neither means buyers can't. after_registering.invoice_details_untilstring- Until when buyers may update their own invoice details,
2006-01-02T15:04on the event's clock, or "". after_registering.holder_changes_stoppedboolean- Whether an attendee may still change their own details.
after_registering.holder_changes_messagestring- What an attendee reads when the window is shut; "" means the shipped sentence, in their own language.
after_registering.holder_email_lockedbooleanafter_registering.holder_details_lockedbooleanafter_registering.self_service_changesboolean- Whether a buyer may remove a ticket or cancel from their own order page. Off by default: it moves money.
after_registering.self_service_untilstring- The cutoff for that, "2006-01-02T15:04" on the event's clock, or "".
after_registering.order_qr_code_enabledboolean- One code per order at the door instead of one per ticket.
after_registering.calendar_links_enabledboolean- Whether people are offered Add to calendar after they book: on their order page, their ticket page and in the emails. On by default. The offer on the event page before booking is a separate switch,
calendar_on_event_pageon the event. after_registering.allow_upgradesboolean- Whether buyers can move a ticket to a dearer ticket type from their order page. An upgrade costs the difference between the two ticket prices, whatever the buyer paid. On by default.
after_registering.allow_downgradesboolean- Whether buyers can move a ticket to a cheaper ticket type from their order page, and get the difference back. Nobody gets back more than they paid for their ticket. Off by default.
checkoutobject- An event's Checkout panel: its own answer to the account's price display and company question, and the link to its terms and conditions.
checkout.tax_displaystring- This event's own answer:
incshows prices including tax,excexcluding it,inheritfollows the account. - one of
inheritincexc checkout.tax_display_excludesboolean- The resolved answer, and the one to read: true when this event's public prices are shown excluding tax.
checkout.ask_companystring- This event's own answer to whether the checkout asks if the buyer is booking for a company.
onlets the buyer tick a box,organizationsmakes every booking an organization's with no box to tick,offnever asks, andinheritfollows the account. - one of
inheritonofforganizations checkout.company_fieldsobject- What an
organizationsevent does with each of an organization's details beyond its name, which is always asked and required:hiddenleaves it off the form,optionalasks for it,requiredrefuses the booking without it. Kept but not applied under any otherask_company. On PATCH, the details named are set and the rest keep their state. checkout.terms_urlstring- The organizer's own terms and conditions page, or an empty string for none. Set, the booking form carries a line above the button at its end saying buyers agree to these terms, linked to this address.
checkout.collect_unfinishedboolean- Collect emails from people who don't finish registering. On, the booking form says "We may email you about this event" above its payment button. A buyer who stops after reading it is listed under Abandoned in Orders and can be messaged until the event ends. A buyer who stopped while it was off is never listed. Off by default.
featuresobject- Which sections this event's admin sidebar shows. A preference, never a kill switch: it hides nothing from a buyer and stops no route.
features.in_usearray of string- Sections something is already using; these show no switch and carry no preference.
{
"registration": {
"sales_end_mode": "ends",
"sales_end_at": null,
"field_collection_timing": "before_payment",
"require_holder_name": false,
"require_holder_email": false,
"require_holder_answers": false,
"qr_gate": "always",
"quotes_enabled": false,
"quotes_on_main": false,
"min_tickets_per_order": 0,
"max_tickets_per_order": 6,
"capacity": 500,
"quote_audience_ids": [
6
]
},
"payment": {
"payment_expiry_days": 0,
"stripe_account": "acct_1MEXAMPLE",
"checkout_mode": "custom",
"hold_minutes": 15,
"stripe_customers_enabled": false,
"bank_transfer_enabled": false,
"bank_transfer_due_days": 14,
"bank_transfer_reminder_days": "1,3,7",
"bank_transfer_overdue_days": "1,7",
"bank_transfer_auto_cancel_days": 0,
"paypal_enabled": false,
"express_checkout_enabled": true,
"extra_currencies": [
"usd",
"gbp"
],
"currency_assignment": ""
},
"after_registering": {
"email_sender_id": 0,
"email_sender_address_id": 0,
"reply_to": "",
"receipt_delivery": "attach",
"invoice_details_days": 30,
"invoice_details_until": "",
"holder_changes_stopped": false,
"holder_changes_until": "",
"holder_changes_message": "",
"holder_email_locked": false,
"holder_details_locked": false,
"self_service_changes": false,
"self_service_until": "",
"order_qr_code_enabled": false,
"calendar_links_enabled": true,
"allow_upgrades": true,
"allow_downgrades": false
},
"checkout": {
"tax_display": "inherit",
"tax_display_excludes": false,
"ask_company": "inherit",
"company_fields": {
"address": "optional",
"country": "required",
"po": "hidden",
"vat": "required"
},
"asks_company": true,
"terms_url": "https://example.com/terms",
"collect_unfinished": false
},
"features": {
"sections": [
"quotes"
],
"available": [
"quotes",
"orders"
],
"in_use": [
"orders"
]
}
}
The PublicPage object
One address this event answers on.
Attributes
{
"kind": "audience",
"id": 6,
"name": "Press",
"address": "/annual/press",
"open": true
}
The EventDetail object
One of the account's event details, and where this event stands on it.
Attributes
labelstring
{
"field_id": 4,
"key": "sponsor",
"label": "Sponsor",
"kind": "text",
"value": "Globex",
"default": "Acme",
"inherited": false
}
The EventDetailsInput object
What PATCH …/settings/details takes.
Attributes
valuesobject- Field id to answer. null falls back to the account's default; "" says this event has nothing to say.
{
"values": {
"4": "Globex",
"5": null
}
}
The EventLanguages object
The languages panel.
Attributes
{
"default": "en",
"extra": [
"ga"
]
}
The EventLanguagesInput object
What PATCH …/settings/languages takes; every field optional.
Attributes
confirmboolean- Accepts the one-way move a new default makes when the event is already translated into it.
{
"default": "ga",
"extra": [
"en"
],
"confirm": true
}
The EventInvitationSettings object
The invitation panel's settings, each with the value in force.
Attributes
settingsobject- Key to value: the asking rules (off/optional/required), the on/off switches, and the refund rules.
{
"settings": {
"cancellation": "off",
"primary_email": "required",
"primary_name": "optional",
"refund_mode": "none"
}
}
The EventInvitationSettingsInput object
What PATCH …/settings/invitations takes; every field optional.
Attributes
settingsobject- The keys to change; "inherit" puts one back to the account's default. Omitted keys keep what they had.
{
"settings": {
"primary_name": "required"
},
"guests_on": true,
"cancellation_on": true
}
The TaxRate object
One tax rate in the account's library.
Attributes
idintegernamestringrate_milli_pctinteger- The percentage in thousandths of a percent: 23000 is 23%, 9975 is 9.975%. Set once, at creation.
rate_bpsinteger- The same percentage in basis points, 2300 for 23%, rounded half up where the rate has a third decimal (9.975% is 998). Charge by
rate_milli_pct. vat_categorystring- The EN 16931 code documents declare for lines carrying this rate; "" is "not stated".
{
"id": 3,
"name": "VAT",
"jurisdiction": "Ireland",
"rate_milli_pct": 23000,
"rate_bps": 2300,
"vat_category": "S",
"exemption_reason": "",
"used_by": 0,
"archived": false
}
The TaxRateInput object
What POST /tax-rates and PATCH /tax-rates/{id} take. On a POST the name, jurisdiction and a rate are required, as rate_milli_pct or rate_bps (or both, when they agree); on a PATCH every field is optional and a rate is refused.
Attributes
namestringjurisdictionstringrate_milli_pctinteger- Create only: 0 to 100000 thousandths of a percent, up to three decimals (9975 is 9.975%).
rate_bpsinteger- Create only: 0 to 10000 basis points. Sent with
rate_milli_pct, it must be that rate rounded half up. exemption_reasonstring- Required by every category except S and Z; ignored where the category needs none.
{
"name": "VAT",
"jurisdiction": "Ireland",
"rate_milli_pct": 23000,
"rate_bps": 2300,
"vat_category": "S",
"exemption_reason": ""
}
The TaxRateDeleted object
What a DELETE answers when the rate was really deleted.
Attributes
{
"id": 3,
"deleted": true
}
The Offering object
One thing an organiser sells beside a ticket.
Attributes
idintegernamestringprice_centsinteger- A good's price; 0 on a donation, which asks rather than charges, and on a time, which a ticket carries.
currencystringsoldintegeron_salebooleanwaitlistboolean- A session offering's waitlist switch: when one of its options is fully booked, people whose ticket includes the offering can wait for it, and a freed place is emailed to them in joining order. Present on a session only; absent on a good, a donation and an archived offering.
waitlist_head_startinteger- A session offering's minutes between one person's offer email and the next: 15, 30, 60, 120, 360 or 1440. Present on a session only.
descriptionstring- A good's or a donation's description, as PATCH takes it. Present on a good and a donation; absent on a session and an archived offering.
tax_rate_idinteger- The tax rate a good or a donation charges, or 0 for no tax. Present on a good and a donation; absent on a session and an archived offering.
price_includes_taxboolean- Whether
price_centsalready contains the tax. Present on a good and a donation; absent on a session and an archived offering. allow_post_orderboolean- Whether a buyer may add it after ordering. Present on a good and a donation; absent on a session and an archived offering.
assign_to_ticketboolean- The buyer is asked which ticket each one is for. Never true on a donation. Present on a good and a donation; absent on a session and an archived offering.
sales_start_atstring (RFC 3339) or null- When it goes on sale, RFC 3339; null for no start. PATCH takes it back as it is. Present on a good and a donation; absent on a session and an archived offering.
sales_end_atstring (RFC 3339) or null- When it comes off sale, RFC 3339; null for no end. Present on a good and a donation; absent on a session and an archived offering.
{
"kind": "good",
"family": "good",
"id": 7,
"name": "T-shirt",
"price_cents": 2000,
"currency": "eur",
"donation_minimum_cents": 0,
"donation_suggested_cents": 0,
"stock": 50,
"available": 50,
"sold": 0,
"standalone": true,
"on_sale": true,
"archived": false,
"runs": 0,
"waitlist": false,
"waitlist_head_start": 60,
"description": "Organic cotton, unisex fit.",
"tax_rate_id": 0,
"price_includes_tax": false,
"allow_post_order": true,
"assign_to_ticket": false,
"sales_start_at": "2030-01-01T09:00:00Z",
"sales_end_at": "2030-02-01T09:00:00Z"
}
The OfferingInput object
What POST /events/{slug}/offerings takes. Which fields apply depends on kind.
Attributes
namestring
{
"kind": "good",
"name": "T-shirt",
"price_cents": 2000,
"donation_minimum_cents": 500,
"donation_suggested_cents": 2500,
"stock": 50,
"standalone": true,
"starts_at": "2030-03-01T10:00",
"ends_at": "2030-03-01T11:00",
"venue": "Room 2",
"capacity": 20
}
The OfferingDeleted object
What a DELETE answers when the offering was really deleted.
Attributes
{
"family": "good",
"id": 7,
"deleted": true
}
The OfferingUpdateInput object
What PATCH /events/{slug}/offerings/{family}/{id} takes; every field optional, and what is not sent is not touched.
Attributes
namestringdescriptionstringassign_to_ticketboolean- The buyer is asked which ticket each one is for. Never true on a donation.
sales_start_atstring or null- When it goes on sale: on the event's clock (
2030-01-01T09:00), or RFC 3339 as the read answers; "" or null for no start. sales_end_atstring or null- When it comes off: on the event's clock, or RFC 3339 as the read answers; "" or null for no end.
waitlistboolean- A session offering's waitlist switch. Switching it on is refused unless the offering is shaped as options. Switching it off keeps everybody already waiting. Refused on a good or a donation.
waitlist_head_startinteger- A session offering's minutes between offer emails: 15, 30, 60, 120, 360 or 1440. Refused on a good or a donation.
{
"name": "Event t-shirt",
"description": "Navy, unisex",
"price_cents": 2500,
"stock": 10,
"standalone": true,
"donation_minimum_cents": 1000,
"donation_suggested_cents": 5000,
"tax_rate_id": 3,
"price_includes_tax": false,
"allow_post_order": true,
"assign_to_ticket": false,
"sales_start_at": "2030-01-01T09:00",
"sales_end_at": "2030-02-01T09:00",
"waitlist": true,
"waitlist_head_start": 60
}
The Translation object
One translatable string.
Attributes
{
"ref": "ticket_type.2.description",
"entity": "ticket_type",
"entity_id": 2,
"field": "description",
"base": "Full weekend access",
"value": "Rochtain deireadh seachtaine",
"published": ""
}
The TranslationsInput object
What PATCH …/translations/{locale} takes.
Attributes
{
"values": {
"event.name": "Imeacht",
"ticket_type.2.description": "Rochtain deireadh seachtaine"
}
}
The TranslationLanguage object
One language this event offers, and how far it has got.
Attributes
localestring
{
"locale": "ga",
"name": "Gaeilge",
"strings": 46,
"translated": 12,
"published": 8
}
The Download object
One file in the event's library.
Attributes
{
"token": "1qR20rFnPEV70bqeGNLJZg",
"filename": "floor-plan.pdf",
"title": "Floor plan",
"description": "Where everything is",
"content_type": "application/pdf",
"size": 20481,
"url": "https://go.tito.io/youraccount/annual/admin/downloads/1qR20rFnPEV70bqeGNLJZg",
"created_at": "2026-09-08T11:00:00Z"
}
The DownloadUpdateInput object
What PATCH …/downloads/{token} takes; both optional.
Attributes
descriptionstring
{
"title": "Hall layout",
"description": "Where everything is"
}
{
"token": "1qR20rFnPEV70bqeGNLJZg",
"deleted": true
}
The BadgeLine object
One line of the stack on the badge.
Attributes
{
"key": "name",
"size": "l"
}
The BadgeSheet object
One stock preset a design can be printed on. The sheets list is the presets that fit the design's paper, so it is shorter on US Letter than on A4. A custom size (sheet "custom") is not in this list: its grid is computed from the typed custom_width_mm and custom_height_mm.
Attributes
{
"key": "8",
"columns": 2,
"rows": 4,
"width_mm": 99,
"height_mm": 68,
"per_sheet": 8
}
{
"key": "a4",
"width_mm": 210,
"height_mm": 297
}
The BadgeDesign object
How this event's badges are drawn.
Attributes
linesarray of BadgeLinelines[].keystring- "name" (the line every badge carries), "type" for the ticket type's own name, or "field:<id>" for an answer to a question this event asked.
lines[].sizestring- Millimetres of type ON the badge, absolute rather than relative to the stock — so changing stock does not re-flow it.
- one of
lms logoboolean- On a positioned design, whether the
logobox is on: the event's uploaded logo, from Customize, printed on each badge. On a stack design it is the flag as last sent, kept but not printed: a stack has no logo. paperstring- One of
papers' keys, or "custom" for a sheet size the organizer typed. It decides which presets are offerable and the page the PDF is converted at. - one of
a4lettercustom custom_width_mmnumber- One badge's width, present only while
sheetis "custom". Always clamped to the paper: a value wider than the page reads back the page's own width. custom_height_mmnumber- One badge's height, present only while
sheetis "custom". Always clamped to the paper. paper_width_mmnumber- The typed paper's width, present only while
paperis "custom". Always clamped to the paper bounds. paper_height_mmnumber- The typed paper's height, present only while
paperis "custom". Always clamped to the paper bounds. orderstring- The order the sheet comes off the printer: by name, or as people booked.
- one of
azbooked fontstring- The font everything on the badge is set in: one of
fonts, the fonts an organizer can choose for their event's pages.interunless the event chose another. - one of
interschibsted-groteskspace-grotesknewsreaderfraunceshost-groteskmanropesource-serif-4instrument-serifibm-plex-monobig-shoulders-displaynunitosource-sans-3 sheetsarray of BadgeSheet- The stock on offer for this
paper, so a caller can picksheetwithout knowing the list. Shorter on US Letter than on A4. sheets[].keystringsheets[].columnsintegersheets[].rowsintegerpapersarray of BadgePaper- The papers on offer, so a caller can pick
paperwithout knowing the list. papers[].keystringpositionedboolean- Whether the design is laid out as positioned boxes rather than as the stack in
lines. For a positioned design,linesis the stack an older reader would print from it. ignoredarray of string- Fields a PATCH accepted and did nothing with. Today only
scale_name, on a positioned design, where a box always fits its text. boxesarray of BadgeBox- The badge as positioned boxes, in millimetres on the design's current card. For a design that is not positioned yet, the layout the badge editor will open it with.
boxes[].keystring- What the box prints:
name,first_nameandlast_name(the name in two parts, read from the buyer's own reading or the event's language),type(the ticket type),field:<id>(an answer),organizer(the account's name, as Settings sets it),logo(the event's logo, as Customize sets it),event(the event name) orref(the ticket reference). Whenfirst_nameandlast_nameare on and one stands above the other, each is printed against the line between them, so the two read as one name with the same small gap on every badge, and whichever is higher prints first. boxes[].same_sizeboolean- Whether the box is set at one size on every badge: the largest the event's longest value fits.
boxes[].size_ptinteger- Present when the box is set at a size of its own, in points: the text prints at it, and smaller only where a long value needs it.
boxes[].match_name_partsboolean- Only on
first_nameandlast_name: whether the two print at one size, the smaller of their two fits. boxes[].stylestring- Absent for
eventandref, which keep the badge foot's own type. - one of
strongplainmuted
{
"saved": false,
"lines": [
{
"key": "name",
"size": "l"
},
{
"key": "type",
"size": "s"
}
],
"logo": false,
"sheet": "8",
"paper": "a4",
"custom_width_mm": 85.6,
"custom_height_mm": 54,
"paper_width_mm": 240,
"paper_height_mm": 120,
"crop": false,
"scale_name": false,
"show_event": true,
"show_ref": true,
"order": "az",
"font": "inter",
"fonts": [
"inter",
"fraunces"
],
"sheets": [
{
"columns": 2,
"height_mm": 68,
"key": "8",
"per_sheet": 8,
"rows": 4,
"width_mm": 99
}
],
"papers": [
{
"height_mm": 297,
"key": "a4",
"width_mm": 210
}
],
"positioned": false,
"ignored": [
"scale_name"
],
"boxes": [
{
"key": "name",
"x_mm": 4,
"y_mm": 20.4,
"width_mm": 91.1,
"height_mm": 10.2,
"wrap": false,
"same_size": false,
"size_pt": 36,
"match_name_parts": true,
"align": "left",
"style": "strong",
"on": true
}
]
}
The BadgeBox object
One element on a positioned badge: where it is and how its text is set.
Attributes
keystring- What the box prints:
name,first_nameandlast_name(the name in two parts, read from the buyer's own reading or the event's language),type(the ticket type),field:<id>(an answer),organizer(the account's name, as Settings sets it),logo(the event's logo, as Customize sets it),event(the event name) orref(the ticket reference). Whenfirst_nameandlast_nameare on and one stands above the other, each is printed against the line between them, so the two read as one name with the same small gap on every badge, and whichever is higher prints first. same_sizeboolean- Whether the box is set at one size on every badge: the largest the event's longest value fits.
size_ptinteger- Present when the box is set at a size of its own, in points: the text prints at it, and smaller only where a long value needs it.
match_name_partsboolean- Only on
first_nameandlast_name: whether the two print at one size, the smaller of their two fits. stylestring- Absent for
eventandref, which keep the badge foot's own type. - one of
strongplainmuted
{
"key": "name",
"x_mm": 4,
"y_mm": 20.4,
"width_mm": 91.1,
"height_mm": 10.2,
"wrap": false,
"same_size": false,
"size_pt": 36,
"match_name_parts": true,
"align": "left",
"style": "strong",
"on": true
}
The BadgeBoxInput object
One box as a caller writes it.
Attributes
keystringx_mmnumbery_mmnumberwidth_mmnumberheight_mmnumberwrapbooleansame_sizebooleansize_ptinteger- A size of the box's own, in points, from 9 to 200: the text prints at it, and smaller only where a long value needs it. 0 or absent sets the text as large as it fits. Overrides
same_size. match_name_partsboolean- Only on
first_nameandlast_name. Defaults to true. False on either sets both to fit their own boxes.
{
"key": "name",
"x_mm": 4,
"y_mm": 20,
"width_mm": 91,
"height_mm": 10,
"wrap": false,
"same_size": false,
"size_pt": 36,
"match_name_parts": true,
"align": "left",
"style": "strong",
"on": true
}
The BadgeDesignInput object
What PATCH …/badge-design takes; every field optional. On a positioned design lines is refused (422 design_is_positioned), show_event and show_ref switch the foot's boxes on or off, and scale_name is accepted and reported in ignored. boxes and lines together are refused (422 boxes_and_lines).
Attributes
boxesarray of BadgeBoxInput- Replaces the positioned layout whole, in millimetres on the design's card, and positions a design that was a stack. The name box must be on (422
no_name_box), whole or split but never both: withfirst_nameorlast_nameon,nameis switched off; a key other thanname,first_name,last_name,type,organizer,logo,event,reforfield:<id>is refused (422unknown_box_key). Boxes are kept inside the card at no less than their minimum size. boxes[].keystringboxes[].x_mmnumberboxes[].y_mmnumberboxes[].width_mmnumberboxes[].height_mmnumberboxes[].wrapbooleanboxes[].same_sizebooleanboxes[].size_ptinteger- A size of the box's own, in points, from 9 to 200: the text prints at it, and smaller only where a long value needs it. 0 or absent sets the text as large as it fits. Overrides
same_size. boxes[].match_name_partsboolean- Only on
first_nameandlast_name. Defaults to true. False on either sets both to fit their own boxes. linesarray of BadgeLine- Replaces the stack whole. The name line is added back at the top if it is missing.
lines[].keystring- "name" (the line every badge carries), "type" for the ticket type's own name, or "field:<id>" for an answer to a question this event asked.
lines[].sizestring- Millimetres of type ON the badge, absolute rather than relative to the stock — so changing stock does not re-flow it.
- one of
lms logoboolean- On a positioned design, switches the
logobox on or off, keeping its place or placing it under what is there. On a stack design it is stored and prints nothing. sheetstring- One of the stock keys offered for
paper, or "custom" for a typed size. Applied afterpaper, so a preset the chosen paper cannot print is refused. paperstring- The sheet the run prints on. Applied before
sheet, because which presets a caller may name depends on it. "custom" takespaper_width_mmandpaper_height_mm. Changing the paper alone can move the stock: a preset the new paper cannot print falls back to the default stock for it, the same way the panel does, and the response says which sheet the design ended up with. Readsheetback rather than assuming it kept the one it had. - one of
a4lettercustom custom_width_mmnumber- One badge's width, meaning anything only while
sheetis "custom". Clamped to the paper rather than refused, the same way the panel clamps it. custom_height_mmnumber- One badge's height, meaning anything only while
sheetis "custom". Clamped to the paper. paper_width_mmnumber- The typed paper's width, meaning anything only while
paperis "custom". Clamped to the paper bounds. paper_height_mmnumber- The typed paper's height, meaning anything only while
paperis "custom". Clamped to the paper bounds. cropbooleanscale_namebooleanshow_eventbooleanshow_refbooleanfontstring- One of the design's
fonts. - one of
interschibsted-groteskspace-grotesknewsreaderfraunceshost-groteskmanropesource-serif-4instrument-serifibm-plex-monobig-shoulders-displaynunitosource-sans-3
{
"boxes": [
{
"key": "name",
"x_mm": 4,
"y_mm": 20,
"width_mm": 91,
"height_mm": 10,
"wrap": false,
"same_size": false,
"size_pt": 36,
"match_name_parts": true,
"align": "left",
"style": "strong",
"on": true
}
],
"lines": [
{
"key": "name",
"size": "l"
}
],
"logo": true,
"sheet": "10",
"paper": "letter",
"custom_width_mm": 85.6,
"custom_height_mm": 54,
"paper_width_mm": 240,
"paper_height_mm": 120,
"crop": true,
"scale_name": true,
"show_event": false,
"show_ref": false,
"order": "booked",
"font": "fraunces"
}
The TerminalReader object
One card reader taking payments on site.
Attributes
collect_nameboolean- Offer an optional name field on the reader before payment. Omit on PATCH to leave unchanged.
collect_emailboolean- Offer an optional email field on the reader before payment. A supplied email receives the Tito order confirmation after payment. Omit on PATCH to leave unchanged.
ticket_type_idsarray of integer or null- Ticket types offered on this Door page. Null offers all saleable types; an empty array offers none. Omit on PATCH to leave unchanged.
idintegerdoor_urlstring- The door the person at the reader opens. On the reader's own record only, and absent once archived.
created_atstring (RFC 3339) or null
{
"collect_name": true,
"collect_email": true,
"ticket_type_ids": [
1
],
"id": 3,
"ref": "R4CAZFD",
"label": "Door 2",
"stripe_reader_id": "tmr_FQ2b0Lm",
"location_id": "tml_main",
"sales": 41,
"archived": false,
"door_url": "https://acct.go.tito.io/door/_9AUNJCBG6eclaz8nCzYqg",
"door_pin": "123456",
"created_at": "2026-09-08T11:40:00Z"
}
The TerminalReaderInput object
What PATCH …/terminal-readers/{id} takes.
Attributes
collect_nameboolean- Offer an optional name field on the reader before payment. Omit on PATCH to leave unchanged.
collect_emailboolean- Offer an optional email field on the reader before payment. A supplied email receives the Tito order confirmation after payment. Omit on PATCH to leave unchanged.
ticket_type_idsarray of integer or null- Ticket types offered on this Door page. Null offers all saleable types; an empty array offers none. Omit on PATCH to leave unchanged.
{
"collect_name": true,
"collect_email": true,
"ticket_type_ids": [
1
],
"label": "Door 2"
}
The StaticPage object
One of an event's Content pages.
Attributes
idintegerurlstring- Where it renders, in the URL shape this request arrived on. With
publishedfalse the piece has this address but it does not open. bodystring- The page as prose: the composer's variables read as their fallbacks, its blocks and marks dropped. What a caller writes and what it gets back.
publishedboolean- Whether the piece's own address opens, and only that. Wherever the piece is placed on a screen or in an email, it shows either way.
ticket_type_idsarray of integer- Who reads the piece where a reader holds tickets. Empty is everyone. Otherwise the order page and the order confirmation show it only for an order with a ticket of one of these types, and the ticket page and the ticket email only for a ticket of one of them. The event page, the booking form and the piece's own address show it to everyone.
has_unpublished_changesboolean- Whether somebody has words for this piece in the admin's editor that are not published yet.
titleandbodyare always the published words. While this is true, a write to the page is refused with409 unpublished_changes. portal_placementsarray of PortalPlacement- Where the piece shows in portals. Set with
POST /events/{slug}/pages/{id}/portal-placements. portal_placements[].idintegerportal_placements[].pagestring- The portal page it sits on.
ticketsis the portal's own link, where the ticket form is.ownis the page as a page of its own in the portal's menu, after Tickets, at the portal's link plus its style name.own:<id>is the page of its own of page<id>. A portal that does not draw the page shows nothing. portal_placements[].positionstring- The top or the bottom of that page. Always
topforown. Onticketsit can also bebefore_issue, just above the portal's Issue tickets heading. - one of
topbefore_issuebottom portal_placements[].every_portalboolean- Every portal of the event, including portals made later, less
hidden_from_portal_ids. Otherwise it shows inportal_idsonly. portal_placements[].portal_idsarray of integer- The portals it shows in, when
every_portalis false. created_atstring (RFC 3339) or nullupdated_atstring (RFC 3339) or null
{
"id": 7,
"path": "getting-here",
"url": "https://acct.go.tito.io/summit/getting-here",
"title": "Getting to the venue",
"body": "The green line stops at St Stephen's Green.",
"body_source": "The green line stops at St Stephen's Green.",
"published": true,
"ticket_type_ids": [],
"has_unpublished_changes": false,
"portal_placements": [
{
"every_portal": true,
"hidden_from_portal_ids": [],
"id": 4,
"page": "tickets",
"portal_ids": [],
"position": "top"
}
],
"created_at": "2026-09-08T12:20:00Z",
"updated_at": "2026-09-08T12:20:00Z"
}
The StaticPageInput object
What POST /events/{slug}/pages takes, and PATCH …/{id}. On a PATCH only the keys present change.
Attributes
pathstring- Optional. Lowercase letters, digits and dashes; one segment, and not a word a real route already answers at. Leave it out and the piece has no address of its own: it is a block, shown where it is placed rather than at a URL.
titlestring- Optional. What the piece is called in the admin, kept to one line. Leave it empty and the admin lists the piece by the start of its words.
publishedboolean- Whether the piece's own address opens. It decides nothing about where the piece is placed. Absent on a create means the address stays shut.
ticket_type_idsarray of integer- Optional. The ticket types the piece is for, each one of this event's. Replaces the stored list. An empty array makes it for everyone again.
{
"path": "getting-here",
"title": "Getting to the venue",
"body": "The green line stops at St Stephen's Green.",
"published": true,
"ticket_type_ids": [
12
]
}
The PortalPlacement object
One place a Content page shows in portals.
Attributes
idintegerpagestring- The portal page it sits on.
ticketsis the portal's own link, where the ticket form is.ownis the page as a page of its own in the portal's menu, after Tickets, at the portal's link plus its style name.own:<id>is the page of its own of page<id>. A portal that does not draw the page shows nothing. positionstring- The top or the bottom of that page. Always
topforown. Onticketsit can also bebefore_issue, just above the portal's Issue tickets heading. - one of
topbefore_issuebottom every_portalboolean- Every portal of the event, including portals made later, less
hidden_from_portal_ids. Otherwise it shows inportal_idsonly.
{
"id": 4,
"page": "tickets",
"position": "top",
"every_portal": false,
"portal_ids": [],
"hidden_from_portal_ids": []
}
The PortalPlacementInput object
Where a Content page shows in portals.
Attributes
pagestring- The portal page:
tickets,details,documentsordownloads.ownmakes it a page of its own in the portal's menu, which needs the page to have a title.own:<id>is the top or bottom of another page's own page. positionstring- Top or bottom of the page. Not needed for
own.before_issueis just above the Issue tickets heading, onticketsonly. - one of
topbefore_issuebottom
{
"page": "tickets",
"position": "top",
"every_portal": false,
"portal_ids": [],
"hidden_from_portal_ids": []
}
The WidgetDomainInput object
What PATCH /widget-domains/{id} takes.
Attributes
{
"status": "approved"
}
The WidgetDomainCreateInput object
What POST /widget-domains takes.
Attributes
domainstring- Required. A host or a URL; it is stored as the bare host, and covers the
www.form too.
{
"domain": "https://www.techarena.se/tickets"
}
The WidgetInput object
What POST /events/{slug}/widgets takes, and PATCH …/{id}. On a PATCH only the keys present change.
Attributes
shapestring- A property of the record, so a widget changes shape without anybody touching the website it is on.
- one of
listbutton all_ticket_types_linkboolean- When the widget lists only some ticket types, end its list with an "All ticket types" link that reopens it showing every ticket type on sale, keeping the quantities already picked. It does nothing on a widget that lists everything. Off by default. It is the same switch as "Let people see every ticket type on sale" on the widget's page in the admin.
full_page_linkboolean- Show an "Open the full page" link in the footer of the widget's checkout, which carries the buyer to the same step on the event's own Tito page, holding the same order. On by default.
show_promo_fieldboolean- Show the promotion code field in the widget, on its ticket list, its order review and the booking form reached from it. Codes still work from their links with it off. On by default. It is the same switch as "Show the promotion code field" on the widget's page in the admin.
prefill_reviewboolean- When the website hands over a buyer's details with tito("widget.create", …), show the filled-in form first instead of going straight on to paying. Off by default. It is the same choice as "Details from your website" on the widget's page in the admin.
openingstring- Accepted and ignored. Every widget opens with
push, whatever is sent here, so a request written whendepthandblurwere choices still succeeds. scopestringalllists everything on sale and is the only way back to it. Naming ticket_type_ids already meanschosen, so this can be left out;allwith a list beside it is refused.- one of
allchosen ticket_type_idsarray of integer- The types to list, in the order to list them. Empty is refused; null is refused.
interest_messagestringbannerstring- What the widget shows across the top of its ticket list:
""for no banner,"event"for the event's own banner (followed as it changes), an https image URL, or"asset:<id>", a picture uploaded on the widget's page in the admin. A picture of the widget's own is uploaded in the admin; here, pass a URL,"event","", or anasset:value the widget already has.
{
"name": "Winter sessions",
"shape": "list",
"secured_line": true,
"analytics": false,
"share_buyer": false,
"all_ticket_types_link": false,
"full_page_link": true,
"show_promo_field": true,
"prefill_review": false,
"opening": "push",
"scope": "chosen",
"ticket_type_ids": [
4,
7
],
"purpose": "purchase",
"interest_message": "example",
"banner": "event",
"show_event_name": true,
"show_date": true,
"show_venue": true,
"show_descriptions": true
}
The AccountInput object
What PATCH /account takes. Only the keys present change; an explicit null is refused.
Attributes
{
"name": "The Sea Church",
"search_indexing": true,
"home_page": "event:summit-2027",
"root_pages": true
}
The Detail object
One of the account's event details.
Attributes
idintegerlabelstringdescriptionstringin_calendarboolean- Whether this detail's value goes into every calendar entry Tito writes for an event, below the event's description. A blank value is left out.
answeredboolean- Whether anything has ever filled it in, which decides whether DELETE destroys or archives.
archivedboolean
{
"id": 5,
"key": "dress_code",
"label": "Dress code",
"kind": "select",
"options": [
"Smart casual",
"Black tie"
],
"description": "What to wear",
"default": "Smart casual",
"in_calendar": false,
"overrides": 3,
"answered": true,
"position": 2,
"archived": false
}
The DetailInput object
What POST /details takes.
Attributes
{
"label": "Dress code",
"kind": "select",
"options": [
"Smart casual",
"Black tie"
],
"description": "What to wear",
"default": "Smart casual",
"in_calendar": true
}
The DetailPatch object
What PATCH /details/{id} takes.
Attributes
in_calendarboolean- Whether this detail's value goes into every calendar entry written for an event.
{
"default": "Black tie",
"in_calendar": true
}
The SellerAddress object
The seller's postal address — the block printed at the top of an invoice.
Attributes
{
"line1": "12 Quay Street",
"line2": "",
"city": "Dungarvan",
"post_code": "X35 YH61",
"subdivision": "Co. Waterford",
"country": "IE"
}
The Checkout object
The account's Checkout panel: the defaults for what a buyer sees and is asked.
Attributes
tax_display_excludesboolean- Public prices are advertised exclusive of tax, on every event without its own answer.
ask_companyboolean- Whether the checkout asks a buyer if they are buying for a company, on every event without its own answer. On for every account that has not turned it off. A buyer who ticks it gives a company name, country and VAT number, and those become the party the invoice is addressed to (see
companyon an order). sends_einvoicesboolean- Whether the checkout also asks for the country-specific identifier an e-invoice needs, on top of the VAT number. OFF for every account that has not turned it on, which is the opposite default to
ask_companyand deliberate: almost no organizer files structured e-invoices. Three countries have such an identifier (Italy, France, Germany) and every other country routes on the VAT number, so for most buyers nothing extra is asked even with this on. Does nothing whileask_companyis off. Account-wide: events have no override. collect_unfinished_new_eventsboolean- Whether a new event starts with
collect_unfinishedswitched on in its own Checkout settings. Moves no event that already exists. Off for every account that has not turned it on.
{
"tax_display_excludes": false,
"ask_company": true,
"sends_einvoices": false,
"collect_unfinished_new_events": false
}
The CheckoutInput object
What PATCH /checkout takes; every field optional.
Attributes
tax_display_excludesboolean- Public prices are advertised exclusive of tax, on every event without its own answer.
ask_companyboolean- Whether the checkout asks a buyer if they are buying for a company, on every event without its own answer. On for every account that has not turned it off. A buyer who ticks it gives a company name, country and VAT number, and those become the party the invoice is addressed to (see
companyon an order). sends_einvoicesboolean- Whether the checkout also asks for the country-specific identifier an e-invoice needs, on top of the VAT number. OFF for every account that has not turned it on, which is the opposite default to
ask_companyand deliberate: almost no organizer files structured e-invoices. Three countries have such an identifier (Italy, France, Germany) and every other country routes on the VAT number, so for most buyers nothing extra is asked even with this on. Does nothing whileask_companyis off. Account-wide: events have no override. collect_unfinished_new_eventsboolean- Whether a new event starts with
collect_unfinishedswitched on in its own Checkout settings. Moves no event that already exists. Off for every account that has not turned it on.
{
"tax_display_excludes": false,
"ask_company": true,
"sends_einvoices": false,
"collect_unfinished_new_events": false
}
The Invoicing object
The account's invoicing settings.
Attributes
seller_address.line1stringseller_address.line2stringseller_address.citystringseller_address.post_codestringseller_address.countrystring- The seller's own 2-letter code, which decides whether the VAT question applies at all.
vat_statusstring- "" is UNSTATED — the default, meaning nobody has answered, never "not registered". An EU seller cannot save while it is unstated.
- one of
registerednot_registered bank_account_holderstring- The account's own bank account, stored exactly as typed — an IBAN is validated by the bank that receives the wire, and a pattern we get wrong is a sale refused.
bank_ibanstring- Holder and IBAN both being filled in is what decides whether buyers are offered bank transfer at all.
bank_bicstringbank_transfer_railstringmanualis the account's own account, reconciled by hand;stripeis a virtual IBAN per buyer.- one of
manualstripe bank_transfer_rail_choice_liveboolean- Whether the choice is live for this account at all: it needs complete bank details, a Stripe connection, and at least one euro event taking transfers. Where this is false the stored rail is not what a buyer would meet.
tax_display_excludesboolean- Public prices are advertised exclusive of tax. The same value
GET /checkoutserves. ask_companyboolean- Whether the checkout asks a buyer if they are buying for a company. On for every account that has not turned it off. A buyer who ticks it gives a company name, country and VAT number, and those become the party the invoice is addressed to (see
companyon an order). Turned off, the question is not on the checkout at all and the fields cannot be posted. sends_einvoicesboolean- Whether the checkout also asks for the country-specific identifier an e-invoice needs, on top of the VAT number. OFF for every account that has not turned it on, which is the opposite default to
ask_companyand deliberate: almost no organizer files structured e-invoices. Three countries have such an identifier (Italy, France, Germany) and every other country routes on the VAT number, so for most buyers nothing extra is asked even with this on. Does nothing whileask_companyis off. vendorobject- The vendor form's company block, by its own field keys (
seller_ein,seller_lei,seller_directors, …, andseller_signatory, the name printed under the signature). On a PATCH only the keys named change, and a key the block does not have is refused. vendor_signatureboolean- Whether vendor forms carry the account's signature, uploaded as an image in Settings, Invoicing. The name printed under it is
seller_signatoryinvendor. Every vendor form is dated the day it is downloaded.
{
"legal_name": "The Sea Church Ltd",
"address_text": "12 Quay St, Dungarvan",
"seller_address": {
"line1": "12 Quay Street",
"line2": "",
"city": "Dungarvan",
"post_code": "X35 YH61",
"subdivision": "Co. Waterford",
"country": "IE"
},
"legal_reg_id": "IE123456",
"vat_status": "registered",
"vat_number": "IE9825613N",
"bank_account_holder": "The Sea Church Ltd",
"bank_iban": "IE29 AIBK 9311 5212 3456 78",
"bank_bic": "AIBKIE2D",
"bank_country": "IE",
"bank_transfer_rail": "manual",
"bank_transfer_rail_choice_live": false,
"tax_display_excludes": false,
"ask_company": true,
"sends_einvoices": true,
"vendor": {
"seller_business_purpose": "Ticketing",
"seller_ein": "12-3456789"
},
"vendor_signature": true
}
The InvoicingInput object
What PATCH /invoicing takes. Only the keys present change; an explicit null is refused.
Attributes
legal_namestringaddress_textstringseller_address.line1stringseller_address.line2stringseller_address.citystringseller_address.post_codestringseller_address.countrystring- The seller's own 2-letter code, which decides whether the VAT question applies at all.
legal_reg_idstringbank_account_holderstringbank_ibanstringbank_bicstringask_companyboolean- Whether the checkout asks a buyer if they are buying for a company. On for every account that has not turned it off. A buyer who ticks it gives a company name, country and VAT number, and those become the party the invoice is addressed to (see
companyon an order). Turned off, the question is not on the checkout at all and the fields cannot be posted. sends_einvoicesboolean- Whether the checkout also asks for the country-specific identifier an e-invoice needs, on top of the VAT number. OFF for every account that has not turned it on, which is the opposite default to
ask_companyand deliberate: almost no organizer files structured e-invoices. Three countries have such an identifier (Italy, France, Germany) and every other country routes on the VAT number, so for most buyers nothing extra is asked even with this on. Does nothing whileask_companyis off. vendorobject- The vendor form's company block, by its own field keys (
seller_ein,seller_lei,seller_directors, …, andseller_signatory, the name printed under the signature). On a PATCH only the keys named change, and a key the block does not have is refused.
{
"legal_name": "The Sea Church Ltd",
"address_text": "12 Quay St, Dungarvan",
"seller_address": {
"line1": "12 Quay Street",
"line2": "",
"city": "Dungarvan",
"post_code": "X35 YH61",
"subdivision": "Co. Waterford",
"country": "IE"
},
"legal_reg_id": "IE123456",
"vat_status": "registered",
"vat_number": "IE9825613N",
"bank_account_holder": "The Sea Church Ltd",
"bank_iban": "IE29 AIBK 9311 5212 3456 78",
"bank_bic": "AIBKIE2D",
"bank_country": "IE",
"bank_transfer_rail": "manual",
"tax_display_excludes": true,
"ask_company": true,
"sends_einvoices": true,
"vendor": {
"seller_business_purpose": "Ticketing",
"seller_ein": "12-3456789"
}
}
The Notifications object
The account's order-notification settings.
Attributes
emailstring- The STORED address; "" means the instance default applies. Kept even while the switch is off.
default_emailstring- The instance's own fallback address, "" where there is none. Read-only — it is the fleet's, not the account's.
period_secondsinteger- The throttle: at most one email per period. 0 is no throttle — every order reports immediately.
localestring- The language the notification is written in, resolved: the stored one, else the admin language, else English.
{
"on": true,
"email": "orders@example.com",
"default_email": "",
"effective_email": "orders@example.com",
"period_seconds": 900,
"locale": "en"
}
The NotificationsInput object
What PATCH /notifications takes. Only the keys present change; an explicit null is refused.
Attributes
{
"on": true,
"email": "orders@example.com",
"period_seconds": 900,
"locale": "en"
}
The ContactSettings object
Whether the account derives an address book from its activity, and from where.
Attributes
{
"on": true,
"from_orders": true,
"from_tickets": true,
"from_invitations": true,
"contact_count": 412
}
The ContactSettingsInput object
What PATCH /contact-settings takes. Only the keys present change; an explicit null is refused.
Attributes
from_ordersboolean- Buyers of pending and paid orders. A canceled order is an abandoned checkout and mints nothing.
{
"on": true,
"from_orders": true,
"from_tickets": true,
"from_invitations": true
}
The Look object
One of the account's looks.
Attributes
idintegerhas_coloursboolean- Whether it carries a manifest of its own. False is a real state — it may carry templates instead.
manifest_tomlstring- Its colours in the form a designer authors, re-encoded from what is stored. Absent from the list, and absent when it has none.
site_header.logo_urlstring- An absolute https:// URL, or a path on the account's own host starting with /.
site_header.logo_altstring- The logo's accessible name, or the words drawn in its place when there is no logo. Required with a logo_url.
site_header.links[].urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
site_header.action.urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
created_atstring (RFC 3339) or null
{
"id": 3,
"name": "Winter 2027",
"has_colours": true,
"files": 12,
"events": 4,
"account_default": false,
"manifest_toml": "[palette]\n background = \"#f6f6fb\"\n card = \"#ffffff\"\n text = \"#3c4257\"\n heading = \"#0a1f44\"\n link = \"#2d40ea\"\n button_bg = \"#2d40ea\"\n button_text = \"#ffffff\"\n\n[shape]\n radius = 0\n shadow = \"none\"\n buttons = \"rounded\"\n\n[type]\n body = \"inter\"\n heading = \"inter\"\n scale = \"regular\"\n [type.custom]\n family = \"\"\n apply = \"\"\n\n[motif]\n band = \"none\"\n section_wash = \"none\"\n wash_color = \"\"\n",
"site_header": {
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"home_url": "https://www.example.com/",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
],
"action": {
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
},
"site_footer": {
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"social": [
{
"kind": "instagram",
"url": "https://www.instagram.com/example"
}
],
"columns": [
{
"heading": "About us",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
]
}
],
"image_url": "/assets/wordmark.svg",
"image_alt": "Example Ltd",
"note": "© Example Ltd"
},
"created_at": "2026-09-08T15:20:00Z"
}
The LookInput object
What POST /looks takes, and PATCH /looks/{id}. On a PATCH only the keys present change.
Attributes
manifest_tomlstring- The colours, fonts, shape and motifs, as a TOML theme file. Every setting it can name, with the values each one takes, is listed at /docs/account/import-a-theme. A setting not on that list is refused (422
invalid_body), with the message naming it and the nearest real setting where one is close. "" clears them.
{
"name": "Winter 2027",
"manifest_toml": "[palette]\nbackground = \"#f6f6fb\"\ncard = \"#ffffff\"\ntext = \"#3c4257\"\nheading = \"#0a1f44\"\nlink = \"#2d40ea\"\nbutton_bg = \"#2d40ea\"\nbutton_text = \"#ffffff\"\n\n[type]\nbody = \"inter\"\nheading = \"inter\"\n",
"site_header": null,
"site_footer": null
}
The SiteLink object
One link in a look's header or footer.
Attributes
urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
The SiteHeader object
A look's own header, drawn above every public page on the look, on the account's own hostname only. Styled by the account's own CSS against site-header.
Attributes
logo_altstring- The logo's accessible name, or the words drawn in its place when there is no logo. Required with a logo_url.
links[].urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
action.urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
{
"logo_url": "/assets/logo.svg",
"logo_alt": "Example Ltd",
"home_url": "https://www.example.com/",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
],
"action": {
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
}
The SiteSocial object
One social profile, drawn as its network's icon.
Attributes
{
"kind": "instagram",
"url": "https://www.instagram.com/example"
}
The SiteColumn object
One titled column of footer links.
Attributes
links[].urlstring- An absolute https:// URL, a path on the account's own host starting with /, or mailto: and an email address.
{
"heading": "About us",
"links": [
{
"label": "Contact",
"url": "https://www.example.com/contact",
"new_tab": false
}
]
}
The FrameHeader object
The site_header design token’s value, sent and read as a JSON string. The header’s picture is the header_logo token, an image like every other. With off true this scope has no header at all, and the rest is kept for when it is turned back on.
Attributes
{
"action": {
"label": "Get tickets",
"url": "https://example.com/register"
},
"home_url": "https://example.com/",
"links": [
{
"label": "Agenda",
"url": "https://example.com/agenda"
}
]
}
{
"deleted": true,
"events_moved": 4
}
The APIKey object
One of the account's API keys. Never its secret.
Attributes
idintegerdisplaystring- The minting prefix plus the stored 8-character fragment — really the secret's leading substring, which is why it ends in an ellipsis. Enough to tell two keys apart, not enough to use.
capabilitiesarray of string- The scope strings this key holds — the same names a developer pastes into an integration. A key is capped by whoever minted it.
revoked_atstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nullparent_key_idinteger or null- The key that delegated this key. Null for keys created in the organizer UI.
{
"id": 2,
"name": "Zapier",
"display": "titogo_sk_A3F9KM2P…",
"capabilities": [
"events.view",
"orders.view"
],
"revoked": false,
"revoked_at": null,
"expired": false,
"expires_at": null,
"last_used_at": "2026-09-08T14:02:00Z",
"created_at": "2026-08-01T09:00:00Z",
"parent_key_id": 1
}
The APIRequest object
One call in the request log. Deliberately carries no request or response body — see the operation.
Attributes
{
"id": 8431,
"key_name": "Zapier",
"method": "GET",
"path": "/api/v1/events/summit-2027/tickets",
"status": 200,
"duration_ms": 14,
"created_at": "2026-09-08T14:02:00Z"
}
The OrderExtraPayment object
A payment an order took and could not use: a second payment for an order already paid, or one that arrived after the order was canceled or refunded. No tickets were issued for it and it is not part of the order's income. It is refunded on its own, from the order's page in the admin. Tito never refunds it automatically.
Attributes
provider_idstring- The provider's own id for the payment: a Stripe PaymentIntent or a PayPal capture. Never the order's own payment.
order_statusstring- What the order was when the payment arrived:
paidis a second payment for an order already paid,canceledandrefundedare a payment that arrived after the order closed. - one of
paidcanceledrefunded
{
"provider": "stripe",
"provider_id": "pi_3PxExample",
"amount_cents": 29800,
"currency": "eur",
"order_status": "paid",
"received_at": "2026-05-18T14:05:40Z",
"refund_id": "",
"refunded_at": null
}
The OrderCompany object
The buyer's own billing identity as they gave it at checkout: the EN 16931 buyer name (BT-44), country (BT-55), VAT identifier (BT-48), postal address (BG-8) and purchase order reference (BT-13). Only the name is required to complete a purchase, so every other field can be an empty string. The VAT number is what was typed; nothing here says it has been verified.
Attributes
vat_numberstring- As typed by the buyer, unnormalized and unverified. Empty when they did not give one — it is never required to complete a purchase.
subdivisionstring- State, province or prefecture. Only collected for countries whose addresses carry one, so it is empty for most of Europe even when the rest of the address is filled in. Where Tito has that country's list (the US, Canada, Australia, Brazil, China, India, Indonesia and Mexico) the buyer picks from it and this is the ISO 3166-2 code, so "OH" rather than "Ohio"; elsewhere it is what they typed.
po_referencestring- The purchase order number the BUYER issued (BT-13), printed on the invoice. Tito's own order number is BT-14 and is
ref. einvoice_idstring- The country-specific routing identifier an e-invoice needs, where the buyer's country has one and the organizer turned
sends_einvoiceson. Read it againstcountry: Italy's is a Codice Destinatario (seven characters) or a PEC address, France's is the buyer's nine-digit SIREN, Germany's is a Leitweg-ID and only a public authority has one. Empty for every other country, because their invoices route onvat_number. Never format-checked at checkout: a wrong value is an invoice that gets re-sent, and refusing the sale would be worse. vat_statusstring- What the EU VAT register said about
vat_number. Empty until there is an answer and empty forever for a country the register does not cover. "unavailable" means we could not GET an answer, not that the number is bad: a member state's registry is down several times a month, and reading it as a verdict would act on an outage. - one of
validinvalidunavailable vat_registered_namestring- The register's own name for the holder of the number, where the member state returns one. It is evidence that the number belongs to
name; nothing compares the two automatically. Empty when the register gave no name, which some member states never do even for a valid number.
{
"name": "Analytical Engines Srl",
"country": "IT",
"vat_number": "IT02659550135",
"address_line1": "Via Ada Byron 12",
"city": "Torino",
"postcode": "10121",
"subdivision": "OH",
"po_reference": "PO-4471",
"einvoice_id": "ABC1234",
"vat_status": "valid",
"vat_registered_name": "ANALYTICAL ENGINES LTD"
}
The MessageFile object
Attributes
filenamestringdeliverystringemailwhen the file is sent with the message,linkwhen the email carries a download link to it instead,offlinewhen Tito took the file offline and it is sent neither way. Decided by the weight of everything attached, in order. An offline file adds no weight.- one of
emaillinkoffline
{
"filename": "floor-plan.pdf",
"size": 284193,
"delivery": "email"
}
The SenderDNSRecord object
One DNS record the account has to publish. Not a credential.
Attributes
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
The EmailSenderList object
Every sending address the account holds.
Attributes
availableboolean- Whether this instance can have senders at all. False means the feature is absent here.
default_fromstring- What mail goes out from when neither the event nor the account has a sender of its own.
senders[].idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. senders[].is_defaultboolean- True for the sender every event whose
email_sender_idis 0 sends from. senders[].configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
senders[].availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
senders[].domainstringsenders[].from_namestring- The display name. At most 100 characters, and never containing a control character.
senders[].statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive senders[].livebooleansenders[].status_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. senders[].not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
senders[].not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
senders[].modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp senders[].recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. senders[].records[].namestringsenders[].records[].valuestringsenders[].records[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. senders[].last_checkedstring (RFC 3339) or nullsenders[].created_atstring (RFC 3339) or nullsenders[].addressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. senders[].addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. senders[].addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. senders[].addresses[].smtp.portintegersenders[].addresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls senders[].addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. senders[].addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
senders[].addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
senders[].addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused senders[].addresses[].created_atstring (RFC 3339) or null
{
"available": true,
"default_from": "tickets@ti.to",
"senders": [
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
]
}
The EmailSender object
The account's custom sending address.
Attributes
idinteger- Names the sender on
/email-senders/{id}, and is what an event'semail_sender_idrefers to. Absent whenconfiguredis false. configuredboolean- False when the account has no sender of its own and mail goes out from the platform's address.
availableboolean- Whether this instance can have one at all — it needs a relay to reach the mail service. False means the feature is absent here.
domainstringfrom_namestring- The display name. At most 100 characters, and never containing a control character.
statusstringliveis all-green only: DKIM, MAIL FROM and the identity itself all verified. There is no partial state. Ansmtpsender islivefrom the moment it is set up, because setting it up needs its server's check to pass.- one of
pendinglive livebooleanstatus_notestring- A short token saying what the last check found when it was not all-green —
dkim,mail_from,unverified, orcheckfor a check that could not complete. not_sendingboolean- A live sender whose last send was refused. Its email goes from Tito's own address, with replies still going to the account's address, until a send from it goes through again.
not_sending_sincestring (RFC 3339) or null- When the sender stopped sending, or null while it sends.
modestring- How the domain sends:
tenantorlegacythrough Tito's mail service,smtpthrough the account's own mail server (each of its addresses then carriessmtp). A domain is one or the other. Absent whenconfiguredis false. - one of
tenantlegacysmtp recordsarray of SenderDNSRecord- The DNS records to publish. Empty when there is no sender, and for an
smtpsender, which has nothing to publish. records[].namestringrecords[].valuestringrecords[].foundboolean- Whether the last check found this record. The mail service checks the DKIM records together and the MAIL FROM pair together, so records in one half always agree. Every record on a live sender reads
true. last_checkedstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladdressesarray of EmailSenderAddress- The From addresses on this domain. The domain is what the mail service verifies, so an address is live exactly when its sender is.
from_localandfrom_nameabove name the domain's primary address; on the default sender that is the account's default address. addresses[].is_defaultboolean- True for the address every event whose
email_sender_address_idis 0 sends as. addresses[].smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. addresses[].smtp.portintegeraddresses[].smtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls addresses[].smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. addresses[].smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
addresses[].smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
addresses[].smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused addresses[].created_atstring (RFC 3339) or null
{
"id": 3,
"is_default": true,
"configured": true,
"available": true,
"default_from": "tickets@ti.to",
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"from_address": "The Summit \u003ctickets@tickets.example.com\u003e",
"status": "pending",
"live": false,
"status_note": "mail_from",
"not_sending": false,
"not_sending_since": null,
"mode": "tenant",
"records": [
{
"type": "CNAME",
"name": "abc123._domainkey.tickets.example.com",
"value": "abc123.dkim.amazonses.com",
"found": true
}
],
"last_checked": "2026-09-09T09:14:00Z",
"created_at": "2026-09-09T09:00:00Z",
"addresses": [
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
]
}
The EmailSenderInput object
What PUT /email-sender and POST /email-senders take: the domain and the first address on it. domain, from_local and from_name are required.
Attributes
reply_tostring- Optional: one plain address replies to this address go to. Empty or absent sends replies to the From address.
{
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"reply_to": ""
}
The EmailSenderSMTP object
The account's own mail server an address on an smtp sender sends through. Absent on every other address. The password is never answered.
Attributes
portintegersecuritystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused
{
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
}
The EmailSenderSMTPInput object
The account's own mail server an address sends through. Tito signs in to it before anything is saved, and keeps the password sealed where only its mail relay can read it: it is never answered and never written to the API's request log.
Attributes
{
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"password": "app-password"
}
The EmailSendersInput object
What POST /email-senders takes: the domain and the first address on it, and, to send through the account's own mail server rather than Tito's mail service, smtp. domain, from_local and from_name are required.
Attributes
domainstring- The domain to send from. Without
smtp, you must be able to publish DNS records on it. reply_tostring- Optional: one plain address replies to this address go to. Empty or absent sends replies to the From address.
smtpobject- The account's own mail server an address sends through. Tito signs in to it before anything is saved, and keeps the password sealed where only its mail relay can read it: it is never answered and never written to the API's request log.
smtp.securitystring- Signing in always needs TLS:
starttlsrequires the server to offer it,tlsconnects over it from the start. - one of
starttlstls smtp.passwordstring- Required, every time: changing any of the server's details asks for the password again.
{
"domain": "tickets.example.com",
"from_local": "tickets",
"from_name": "The Summit",
"reply_to": "",
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"password": "app-password"
}
}
The EmailSenderAddress object
One From address on a sender's domain.
Attributes
smtpobject- The account's own mail server an address on an
smtpsender sends through. Absent on every other address. The password is never answered. smtp.portintegersmtp.securitystringstarttlsupgrades the connection before signing in,tlsconnects over TLS from the start (usually port 465). A server that offers neither is refused.- one of
starttlstls smtp.checked_atstring (RFC 3339) or null- When the server last accepted the sign-in: when it was set up, or
POST /email-senders/{id}/addresses/{address_id}/smtp/check. smtp.not_sendingboolean- The server refused this address in a way the account can fix: the sign-in, TLS, a host Tito won't connect to, or the address itself. Its email goes from Tito's own address, with replies still going to the account's address, until a send or a check goes through again. A server that is down or slow does not set it.
smtp.not_sending_sincestring (RFC 3339) or null- When the address stopped sending, or null while it sends.
smtp.not_sending_codestring- Why, while
not_sendingis true:auth(the sign-in was refused),tls,blocked(the host resolves somewhere Tito won't connect to) orsender_refused(the server won't send as this address). - one of
authtlsblockedsender_refused created_atstring (RFC 3339) or null
{
"id": 7,
"from_local": "forum",
"from_name": "The Summit Forum",
"address": "forum@tickets.example.com",
"from_address": "The Summit Forum \u003cforum@tickets.example.com\u003e",
"reply_to": "",
"is_default": false,
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"checked_at": "2026-10-21T09:00:00Z",
"not_sending": false,
"not_sending_since": null,
"not_sending_code": "auth"
},
"created_at": "2026-09-29T09:00:00Z"
}
The EmailSenderAddressInput object
What POST /email-senders/{id}/addresses takes. smtp is required for a new address on an smtp sender, and refused on any other.
Attributes
reply_tostring- Optional: one plain address replies go to. Empty or absent sends replies to the From address.
smtpobject- The account's own mail server an address sends through. Tito signs in to it before anything is saved, and keeps the password sealed where only its mail relay can read it: it is never answered and never written to the API's request log.
smtp.securitystring- Signing in always needs TLS:
starttlsrequires the server to offer it,tlsconnects over it from the start. - one of
starttlstls smtp.passwordstring- Required, every time: changing any of the server's details asks for the password again.
{
"from_local": "forum",
"from_name": "The Summit Forum",
"reply_to": "",
"smtp": {
"host": "smtp.example.com",
"port": 587,
"security": "starttls",
"username": "tickets@tickets.example.com",
"password": "app-password"
}
}
The WebhookInput object
What POST /webhooks takes. There is no secret field: one is minted for you and read off the organizer's own screen.
Attributes
event_typesarray of string- At least one of the events this account sends. An empty list is refused.
{
"url": "https://example.com/tito-hook",
"event_types": [
"order.paid",
"ticket.created"
]
}
The WebhookTestResult object
What a test delivery came back with.
Attributes
{
"delivery_id": 88,
"status": "succeeded",
"status_code": 200,
"error": ""
}
The WorkflowInput object
What POST /workflows takes, and PATCH /workflows/{id}. On a PATCH only the keys present change. There is no definition field: see the operation notes.
Attributes
templatestring- Create only: stamps a whole working definition by name. The copy is the account's own from that moment — no link back, so a later change to the template never reaches a live config.
eventsarray of string- The slugs of the events this workflow runs for. Replaces the ones it covers, the same as ticking them in the builder, and an empty list covers none. A workflow fires only for the events it covers, so a template stamped without this or
all_eventsis switched on to no effect. A slug this account does not have refuses the whole request with422 invalid_body. all_eventsbooleantruecovers every event, including ones made later. Sent together witheventsit is refused.falseon its own covers none.enabledboolean- Patch only. Turning it on is refused unless the workflow could actually run; turning it off also stops the queued work.
{
"name": "Slack when a ticket sells",
"template": "slack_order_paid",
"events": [
"my-event"
],
"all_events": false,
"enabled": true
}
{
"id": "acct_1MEXAMPLE",
"name": "Example Events Ltd",
"livemode": true,
"default": true,
"events": 4
}
The Connection object
One third-party service and its connection status. The account list includes only account providers. The event list includes only providers scoped to that event.
Attributes
categorystring- What kind of service it is, from the relay's manifest — "payments", "networking", "lead-retrieval" and so on.
kindstring- How this service is connected:
oauth,pat(a pasted key),both, orpartner_referral. Use the corresponding credential or authorization API route for this provider kind. connectedbooleanconnected_atstring (RFC 3339)stripe_accountsarray of StripeAccount- Stripe only: every Stripe account this account holds, the first connected first.
account_idis the default among them, the one new events pay into. Absent for every other service. stripe_accounts[].idstringaccount_idstring- The provider-side identifier worth showing: a Stripe account or a PayPal merchant id. Not a credential — it is on the organizer's own screen and in the provider's dashboard — and empty for a service that has neither.
sheetobject- The spreadsheet a Google Sheets connection writes to, null until one has been made. The id is not a secret: it is in the address bar of anyone who opens the sheet.
sheet.idstringsheet.urlstringlivemodeboolean- On a connected Stripe row: true for a live Stripe account, which takes real payments, and false for a Stripe sandbox, which takes test payments only. Null on every other row, and on a Stripe row that is not connected.
{
"id": "stripe",
"name": "Stripe",
"category": "payments",
"kind": "oauth",
"connected": true,
"connected_at": "2026-09-01T10:04:00Z",
"stripe_accounts": [
{
"id": "acct_1MEXAMPLE",
"name": "Example Events Ltd",
"livemode": true,
"default": true,
"events": 4
}
],
"account_id": "acct_1MEXAMPLE",
"sheet": {
"id": "1AbCdEf",
"url": "https://docs.google.com/spreadsheets/d/1AbCdEf/edit"
},
"livemode": true
}
The DocumentNumberingKind object
One document kind's series.
Attributes
prefixstring- The TEMPLATE, not the rendered prefix: it may carry
{YYYY},{YY},{MM}and{EVENT}, over a deliberately narrow alphabet (letters, digits and- _ / . #and a space). This string is printed on an accounting document and typed into other people's systems. resetstringnever, orannualfor a counter that restarts each January. An annual reset needs the year in the prefix or the suffix.suffixstring- What comes after the count, a template like
prefixwith the same placeholders and alphabet. It starts with a letter or a mark, never a digit or a placeholder, so it can't run into the count.""is no suffix. nextstring- What the next number of this kind WOULD look like. Reading it allocates nothing. Counted against the account's newest event, which is what a per-event series means anything against.
{
"kind": "invoice",
"prefix": "INV-{YYYY}-",
"pad": 5,
"reset": "annual",
"suffix": "-{YY}",
"next": "INV-2026-00043"
}
The DocumentNumbering object
The document numbering series: what each kind of document is CALLED, and the two account-wide answers beside them.
Attributes
scopestringaccount(the default, and what a tax authority generally expects — an event is a Tito concept, not a legal one) orevent.issued_countinteger- How many numbers this account has printed — the REASON the scope is fixed. "240 numbers have been issued" is a reason; "this is fixed" is an assertion.
zero_valuestring- What a zero-value order gets:
number(the default — a free registration is a real registration),unnumbered(a real document identified by its order ref, which spends no number and so locks nothing) ornone. kindsarray of DocumentNumberingKindkinds[].prefixstring- The TEMPLATE, not the rendered prefix: it may carry
{YYYY},{YY},{MM}and{EVENT}, over a deliberately narrow alphabet (letters, digits and- _ / . #and a space). This string is printed on an accounting document and typed into other people's systems. kinds[].resetstringnever, orannualfor a counter that restarts each January. An annual reset needs the year in the prefix or the suffix.kinds[].suffixstring- What comes after the count, a template like
prefixwith the same placeholders and alphabet. It starts with a letter or a mark, never a digit or a placeholder, so it can't run into the count.""is no suffix. kinds[].nextstring- What the next number of this kind WOULD look like. Reading it allocates nothing. Counted against the account's newest event, which is what a per-event series means anything against.
{
"scope": "account",
"scope_locked": true,
"issued_count": 240,
"zero_value": "number",
"kinds": [
{
"kind": "invoice",
"prefix": "INV-{YYYY}-",
"pad": 5,
"reset": "annual",
"suffix": "-{YY}",
"next": "INV-2026-00043"
}
]
}
The EventDocumentNumbering object
An event's own document numbering (PRO-5786).
Attributes
ownboolean- Whether this event uses its own prefixes. False means it draws from the account's series.
account_numberedinteger- How many of this event's documents already carry a number from the account's series. Switching never changes them.
kindsarray of DocumentNumberingKind- One per kind.
prefixis blank for a kind nobody has typed one for, andnextis blank until there is a prefix. kinds[].prefixstring- The TEMPLATE, not the rendered prefix: it may carry
{YYYY},{YY},{MM}and{EVENT}, over a deliberately narrow alphabet (letters, digits and- _ / . #and a space). This string is printed on an accounting document and typed into other people's systems. kinds[].resetstringnever, orannualfor a counter that restarts each January. An annual reset needs the year in the prefix or the suffix.kinds[].suffixstring- What comes after the count, a template like
prefixwith the same placeholders and alphabet. It starts with a letter or a mark, never a digit or a placeholder, so it can't run into the count.""is no suffix. kinds[].nextstring- What the next number of this kind WOULD look like. Reading it allocates nothing. Counted against the account's newest event, which is what a per-event series means anything against.
{
"own": true,
"account_numbered": 3,
"kinds": [
{
"kind": "invoice",
"prefix": "INV-{YYYY}-",
"pad": 5,
"reset": "annual",
"suffix": "-{YY}",
"next": "INV-2026-00043"
}
]
}
The EventDocumentNumberingInput object
What PATCH /events/{slug}/settings/numbering takes.
Attributes
ownboolean
{
"own": true,
"kinds": {}
}
The DocumentNumberingInput object
What PATCH /invoicing/numbering takes. Only the keys present change, and inside kinds only the fields present change — a kind named with just a prefix keeps its stored padding and reset rule, and a kind not named at all is untouched.
Attributes
{
"scope": "event",
"zero_value": "unnumbered",
"kinds": {}
}
The ContactList object
One list in the address book. There is no delete, on this surface or the organizer's: a list somebody was added to is a record of who was told what, so a list is ARCHIVED — which keeps its memberships exactly as they stand and takes it off every chooser.
Attributes
{
"id": 4,
"name": "Newsletter",
"archived": false,
"member_count": 128,
"created_at": "2026-10-14T09:00:00Z"
}
The Contact object
One person in the account's address book, keyed by email. A contact's activity is not served here. The organizer's own page shows the orders, tickets and invitations matching their address — records that orders.view and attendees.view gate. A key holding only the address book must not read the account's sales through it, so neither the feed nor the per-person counts are on these answers.
Attributes
emailstringnamestringorgstringphonestringnotestringsourcestring- How this person first arrived:
manualfor somebody added deliberately, or the sweep's own word for an order, a ticket or an invitation. Stamped on first sighting and never rewritten. listsarray of ContactListlists[].idintegerlists[].namestringlists[].archivedbooleanlists[].member_countintegerlists[].created_atstring (RFC 3339)created_atstring (RFC 3339)updated_atstring (RFC 3339)
{
"ref": "c1a2b3",
"email": "ada@example.com",
"name": "Ada Lovelace",
"org": "Analytical Engines",
"phone": "example",
"note": "example",
"source": "manual",
"tags": [
"speaker"
],
"lists": [
{
"id": 4,
"name": "Newsletter",
"archived": false,
"member_count": 128,
"created_at": "2026-10-14T09:00:00Z"
}
],
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z"
}
The ContactInput object
What POST /contacts takes, and PATCH /contacts/{ref}. On a PATCH only the keys present change: the identity is stored WHOLE, so a request naming one field folds over what is already there rather than blanking the rest.
Attributes
emailstringnamestringorgstringphonestringnotestringtagsarray of string- A SET: naming it replaces what is there, omitting it leaves it, and an empty array clears it.
listsarray of integer- A SET of list ids, the same way. Archived lists are refused: they are off every chooser the organizer has, so adding to one here would be the one way to grow a list nobody can see. Memberships of an archived list are left exactly as they stand.
{
"email": "ada@example.com",
"name": "Ada Lovelace",
"org": "example",
"phone": "example",
"note": "example",
"tags": [
"speaker"
],
"lists": [
4
]
}
The ContactListInput object
What POST /contact-lists takes, and PATCH /contact-lists/{id}. Only the keys present change.
Attributes
namestring
{
"name": "Newsletter",
"archived": true
}
The BuyerAccount object
One buyer in the account's register, such as a company or an organization. Buyers carry no event: this is an account-wide record, which is what makes "show me everything for this buyer" answerable.
Attributes
refstringemailstringnamestringorgstringphonestring- The buyer's phone number as it was typed, trimmed and cut to 64 characters. It isn't checked or reformatted. Empty when the buyer has none.
quote_countinteger- Quotes matching this buyer's id OR their email — an anonymous quote is still theirs.
order_countinteger- Orders matching their email, case-insensitively: buyers.email is lowercased on the way in and an order keeps the address as the buyer typed it, so a case-sensitive match once read a registered buyer as having no orders at all.
part_ofstring or null- The ref of the buyer this one is part of, like a department's company, or null when it is part of nothing. One level: a buyer others are part of is never part of another.
kindobject or null- The kind a buyer is, or null for none. A kind is a label for finding and filtering buyers; nothing else depends on it. The whole kind is
GET /buyer-kinds/{id}. kind.idintegerkind.namestringcreated_atstring (RFC 3339) or nulladmin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
{
"ref": "b7x9k2",
"email": "accounts@example.com",
"name": "Alex Smith",
"org": "Example Ltd",
"phone": "+1 202 555 0143",
"quote_count": 3,
"order_count": 5,
"part_of": "k3m8p1",
"kind": {
"id": 2,
"name": "Partners"
},
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito",
"anonymized_at": null
}
The BuyerBlock object
Whether the buyer is blocked from starting new orders and quotes, by its own block or the one on the buyer it is part of. Set it with POST /buyers/{ref}/block and lift it with DELETE /buyers/{ref}/block.
Attributes
{
"blocked": true,
"message": "Please settle your outstanding invoices with our accounts team before booking again.",
"blocked_at": "2026-10-21T11:30:00Z",
"by": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
}
The BuyerAccountDetail object
Attributes
refstringemailstringnamestringorgstringphonestring- The buyer's phone number as it was typed, trimmed and cut to 64 characters. It isn't checked or reformatted. Empty when the buyer has none.
quote_countinteger- Quotes matching this buyer's id OR their email — an anonymous quote is still theirs.
order_countinteger- Orders matching their email, case-insensitively: buyers.email is lowercased on the way in and an order keeps the address as the buyer typed it, so a case-sensitive match once read a registered buyer as having no orders at all.
part_ofstring or null- The ref of the buyer this one is part of, like a department's company, or null when it is part of nothing. One level: a buyer others are part of is never part of another.
kindobject or null- The kind a buyer is, or null for none. A kind is a label for finding and filtering buyers; nothing else depends on it. The whole kind is
GET /buyer-kinds/{id}. kind.idintegerkind.namestringcreated_atstring (RFC 3339) or nulladmin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
anonymized_atstring (RFC 3339) or null- When the person on this record was anonymized, null while they are still on it. An anonymized record keeps its reference, its place and its money and has no name or address, which this tells apart from one nobody filled in.
stripe_customer_idstring- The Customer this buyer is on Stripe, empty until something mints one (the bank-transfer path does it lazily). An identifier, not a credential.
people[].emailstringpeople[].primaryboolean- The buyer's primary contact: the address it is listed under, the one invoices and Stripe use. It can't be removed; make someone else primary first (
POST /buyers/{ref}/primary). people[].rolestring- What this person can do when the buyer's quotes need approval: a requester sends a quote for approval, an approver can reject one. Where approval isn't needed the two can do the same things.
- one of
requesterapprover people[].sees_allboolean- Whether this person sees every one of the buyer's bookings and quotes when they sign in, or only the ones made with their own email address. Only the organizer can change it (
POST /buyers/{ref}/people/{email}/sees). domainsarray of string- The buyer's email domains, sorted, never null. Addresses at exactly these domains belong to the buyer before anybody signs in.
termsobject or null- The named payment terms a buyer has of its own, or null when it has none. A buyer without terms of its own follows the terms of the buyer it is part of (
inherited_termson the buyer), or else each event's own bank transfer settings. The whole set isGET /payment-terms/{id}. terms.idintegerterms.namestringinherited_termsobject- What a buyer without terms of its own follows: the terms of the buyer it is part of. Null when it has its own, is part of nothing, or that buyer has none either.
inherited_terms.idintegerinherited_terms.namestringincludes[].refstringincludes[].namestringpo_requiredboolean- This buyer's own approval setting: its quotes, and those of every buyer part of it, go to an approver before they can become orders. Always false on a buyer that is part of another, which follows that buyer's;
approvalsays what applies. Set it withPOST /buyers/{ref}/po-required. approvalobject- Approval as it applies to this buyer: required or not, and the buyer whose approvers answer, which is the buyer this one is part of, or else itself.
approval.requiredbooleanapproval.approverobjectapproval.approver.refstringapproval.approver.namestringblockobject- Whether the buyer is blocked from starting new orders and quotes, by its own block or the one on the buyer it is part of. Set it with
POST /buyers/{ref}/blockand lift it withDELETE /buyers/{ref}/block. block.blockedbooleanblock.messagestring- The words emailed to whoever at the buyer tries to order, as plain text, empty for the standard line.
block.blocked_atstring (RFC 3339) or null- When the block was set, null when the buyer isn't blocked.
block.byobject or null- The buyer whose block applies: this one, or the one it is part of. Null when the buyer isn't blocked.
block.by.refstringblock.by.namestringspendarray of object- What this buyer has actually PAID, per currency: order totals less whatever was refunded off them. A buyer can span currencies across events, so it is a list. A partial refund leaves the order paid and is netted; a full one flips its status and nets to zero either way.
spend[].centsintegerspend[].currencystringquotesarray of BuyerQuoteSummaryquotes[].refstringquotes[].event_slugstringquotes[].statusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
quotes[].amount_centsintegerquotes[].currencystringquotes[].expires_atstring (RFC 3339) or nullquotes[].created_atstring (RFC 3339) or nullquotes[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
ordersarray of BuyerOrderSummaryorders[].refstringorders[].event_slugstringorders[].statusstringorders[].amount_centsintegerorders[].currencystringorders[].payment_methodstringorders[].created_atstring (RFC 3339) or nullorders[].admin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
bank_transferobject or null- The newest wire-instruction snapshot, null until this buyer has one. Read from the stored row — no Stripe call — because the virtual IBAN is per-Customer, so that row IS the account they pay into. The IBAN is here deliberately: it is the account's own receiving account, printed on the invoice the buyer was already sent, not a secret that opens anything.
bank_transfer.referencestringbank_transfer.ibanstringbank_transfer.bicstringbank_transfer.account_holderstringbank_transfer.bank_countrystringbank_transfer.amount_centsintegerbank_transfer.currencystringbank_transfer.due_atstring (RFC 3339) or nullbank_detailsobject or null- The bank details Stripe issued this buyer's current Stripe customer, kept on the buyer: the account the buyer pays every bank transfer into, and what its vendor form prints. Null until they are kept, either by
POST /buyers/{ref}/bank-detailsor by the buyer's first Stripe bank transfer. Details kept for a Stripe customer the buyer is no longer linked to are never served. Read from the stored row, with no Stripe call. bank_details.account_holderstringbank_details.ibanstringbank_details.bicstringbank_details.currencystringbillingobject- This customer's billing identity, as currently stored (never a computed fallback — a blank
namehere can still print asorgon an invoice). billing.namestring- The legal entity an invoice is addressed to. Blank falls back to
orgwhen an invoice is rendered. billing.line1stringbilling.line2stringbilling.citystringbilling.postcodestringbilling.vat_numberstring
{
"ref": "b7x9k2",
"email": "accounts@example.com",
"name": "Alex Smith",
"org": "Example Ltd",
"phone": "+1 202 555 0143",
"quote_count": 3,
"order_count": 5,
"part_of": "k3m8p1",
"kind": {
"id": 2,
"name": "Partners"
},
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito",
"anonymized_at": null,
"stripe_customer_id": "cus_1MEXAMPLE",
"people": [
{
"email": "accounts@example.com",
"primary": true,
"role": "approver",
"sees_all": true
}
],
"domains": [
"example.org"
],
"terms": {
"id": 3,
"name": "Members"
},
"inherited_terms": {
"id": 3,
"name": "Members"
},
"includes": [
{
"ref": "p2q7r4",
"name": "Example Ltd Design"
}
],
"po_required": false,
"approval": {
"required": true,
"approver": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
},
"block": {
"blocked": true,
"message": "Please settle your outstanding invoices with our accounts team before booking again.",
"blocked_at": "2026-10-21T11:30:00Z",
"by": {
"ref": "k3m8p1",
"name": "Example Ltd"
}
},
"spend": [
{
"cents": 150000,
"currency": "eur"
}
],
"quotes": [
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "open",
"amount_cents": 14900,
"currency": "eur",
"order_ref": "H7K2PX",
"expires_at": "2026-10-14T09:00:00Z",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
],
"orders": [
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "paid",
"amount_cents": 14900,
"currency": "eur",
"payment_method": "example",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
],
"bank_transfer": {
"reference": "RF97TESTREF",
"iban": "example",
"bic": "example",
"account_holder": "Ada Lovelace",
"bank_country": "example",
"amount_cents": 14900,
"currency": "eur",
"status": "awaiting",
"due_at": "2026-10-14T09:00:00Z"
},
"bank_details": {
"account_holder": "Stripe Payments Europe",
"iban": "IE29AIBK93115212345678",
"bic": "AIBKIE2D",
"bank_country": "IE",
"currency": "eur"
},
"billing": {
"name": "St Brigid's NS Board of Management",
"line1": "12 Main Street",
"line2": "",
"city": "Springfield",
"postcode": "D02 AF30",
"subdivision": "Co. Dublin",
"country": "IE",
"vat_number": "IE1234567T",
"buyer_reference": "",
"po_reference": ""
}
}
The BuyerBilling object
A customer's billing identity: who an invoice is made out to and its postal address, plus the buyer's own references. Set directly by an organizer (PATCH /buyers/{ref}, POST /buyers), by the buyer import, or by the buyer's own invoice-request form. Every field is blank-safe — an invoice simply omits a line it doesn't have.
Attributes
{
"name": "St Brigid's NS Board of Management",
"line1": "12 Main Street",
"line2": "",
"city": "Springfield",
"postcode": "D02 AF30",
"subdivision": "Co. Dublin",
"country": "IE",
"vat_number": "IE1234567T",
"buyer_reference": "",
"po_reference": ""
}
The BuyerPerson object
One person who signs in as the buyer.
Attributes
emailstringprimaryboolean- The buyer's primary contact: the address it is listed under, the one invoices and Stripe use. It can't be removed; make someone else primary first (
POST /buyers/{ref}/primary). rolestring- What this person can do when the buyer's quotes need approval: a requester sends a quote for approval, an approver can reject one. Where approval isn't needed the two can do the same things.
- one of
requesterapprover sees_allboolean- Whether this person sees every one of the buyer's bookings and quotes when they sign in, or only the ones made with their own email address. Only the organizer can change it (
POST /buyers/{ref}/people/{email}/sees).
{
"email": "accounts@example.com",
"primary": true,
"role": "approver",
"sees_all": true
}
The BuyerTerms object
The named payment terms a buyer has of its own, or null when it has none. A buyer without terms of its own follows the terms of the buyer it is part of (inherited_terms on the buyer), or else each event's own bank transfer settings. The whole set is GET /payment-terms/{id}.
Attributes
{
"id": 3,
"name": "Members"
}
The BuyerKind object
The kind a buyer is, or null for none. A kind is a label for finding and filtering buyers; nothing else depends on it. The whole kind is GET /buyer-kinds/{id}.
Attributes
{
"id": 2,
"name": "Partners"
}
The BuyerKindDetail object
One of the account's kinds: a named label buyers are sorted into, like Partners or Members.
Attributes
{
"id": 2,
"name": "Partners",
"buyer_count": 12
}
The PaymentTerms object
A named set of payment terms: when an invoice issued under them is due, when the buyer is reminded and chased, and whether an unpaid invoice cancels itself.
Attributes
idintegeranchorstring- What
dayscounts from.ordercounts forward from the order.eventcounts forward from the day the event ends (or starts, when it has no end); an order that comes in later than that counts from the order instead, so nobody is due before they booked.before_eventcounts back from the day the event starts; an order that comes in later than that is due at the end of the day it was placed. An event with no date at all can't have an invoice issued under either event anchor. - one of
ordereventbefore_event reminder_daysstring- Remind the buyer this many days before the due date, comma-separated. Empty for no reminders.
overdue_daysstring- Chase the buyer this many days after the due date, comma-separated. Empty for no chasing.
auto_cancel_daysinteger- Cancel the order this many days after the due date, putting its tickets back on sale. 0 is off.
buyer_countinteger- How many buyers' invoices are issued under these terms: those with them as their own, and those without terms of their own that are part of one that has them.
{
"id": 3,
"name": "Members",
"anchor": "event",
"days": 7,
"reminder_days": "7,1",
"overdue_days": "1,7",
"auto_cancel_days": 0,
"buyer_count": 12
}
The BuyerQuoteSummary object
One of this buyer's quotes, enough to recognise it. The whole quote is GET /quotes/{ref}.
Attributes
refstringevent_slugstringstatusstring- open, submitted (sent for approval), rejected (an approver turned it down), imported (converted to an order), expired or canceled.
amount_centsintegercurrencystringexpires_atstring (RFC 3339) or nullcreated_atstring (RFC 3339) or nulladmin_urlstring- The organizer page, as an absolute URL: on the root admin origin, or on the instance's own address on a standalone instance.
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "open",
"amount_cents": 14900,
"currency": "eur",
"order_ref": "H7K2PX",
"expires_at": "2026-10-14T09:00:00Z",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
The BuyerOrderSummary object
One of this buyer's orders. The whole order is GET /orders/{ref}.
Attributes
{
"ref": "H7K2PX",
"event_slug": "demo",
"status": "paid",
"amount_cents": 14900,
"currency": "eur",
"payment_method": "example",
"created_at": "2026-10-14T09:00:00Z",
"admin_url": "https://example.com/hooks/tito"
}
The GroupCheckin object
Attributes
{
"order_ref": "H7K2PX",
"checked_in": 1,
"rows": [
{
"ticket_type_id": 1,
"ticket_type_name": "T9QW4M",
"total": 42,
"checked_in": 1,
"remaining": 1
}
]
}
The ReportValues object
Attributes
{
"key": "example",
"label": "example",
"values": {}
}
The SavedReportResults object
Attributes
reportobjectreport.idinteger- The report's row id; saved reports carry no ref (admin-only, never a public surface).
report.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).
report.created_atstring (RFC 3339)- Integer Unix timestamp in storage, rendered as RFC 3339 UTC. Not nullable (NOT NULL column).
report.updated_atstring (RFC 3339)- When the name or the definition last changed. Equals
created_aton a report nobody has edited. querystringtimezonestringcurrencystringcolumnsarray of objectcolumns[].labelstringrowsarray of ReportValuesrows[].keystringrows[].labelstringrows[].valuesobject- Integer measures keyed by the column key. Money is in currency minor units; percentages are rounded whole percentage points.
totalsobjecttotals.keystringtotals.labelstringtotals.valuesobject- Integer measures keyed by the column key. Money is in currency minor units; percentages are rounded whole percentage points.
{
"report": {
"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",
"share": null
},
"query": "example",
"source": "orders",
"timezone": "Europe/Dublin",
"currency": "eur",
"columns": [
{
"key": "example",
"label": "example",
"kind": "count"
}
],
"rows": [
{
"key": "example",
"label": "example",
"values": {}
}
],
"totals": {
"key": "example",
"label": "example",
"values": {}
}
}
The AttendeeRescheduleTime object
Attributes
{
"id": 1,
"available": 1,
"name": "Ada Lovelace",
"starts_at": "2026-10-14T09:00:00Z",
"ends_at": "2026-10-14T09:00:00Z",
"full": true,
"short": true,
"closed": true,
"clash": true
}
The AttendeeRescheduleSeat object
Attributes
idintegerentitlement_idintegerentitlement_namestringunitsintegercurrentobjectcurrent.idintegercurrent.availableintegercurrent.namestringcurrent.starts_atstringcurrent.ends_atstringcurrent.fullbooleancurrent.shortbooleancurrent.closedbooleancurrent.clashbooleantargetsarray of AttendeeRescheduleTimetargets[].idintegertargets[].availableintegertargets[].namestringtargets[].starts_atstringtargets[].ends_atstringtargets[].fullbooleantargets[].shortbooleantargets[].closedbooleantargets[].clashboolean
{
"id": 1,
"entitlement_id": 1,
"entitlement_name": "Ada Lovelace",
"units": 1,
"current": {
"id": 1,
"available": 1,
"name": "Ada Lovelace",
"starts_at": "2026-10-14T09:00:00Z",
"ends_at": "2026-10-14T09:00:00Z",
"full": true,
"short": true,
"closed": true,
"clash": true
},
"targets": [
{
"id": 1,
"available": 1,
"name": "Ada Lovelace",
"starts_at": "2026-10-14T09:00:00Z",
"ends_at": "2026-10-14T09:00:00Z",
"full": true,
"short": true,
"closed": true,
"clash": true
}
]
}
The FreeTicketRule object
Attributes
{
"ticket_type_id": 1,
"qualifying_quantity": 2,
"free_quantity": 2
}
The EventSyncTarget object
Attributes
{
"service": "example",
"name": "Ada Lovelace",
"connected": true,
"settings": {},
"schema": [
{
"key": "example",
"kind": "text",
"required": true
}
],
"type_map": {}
}
The EventPageOrder object
Attributes
{
"order": [
"H7K2PX"
],
"max_sections": 1,
"sections": [
{
"token": "example",
"id": 1,
"name": "Ada Lovelace",
"hidden": true
}
],
"items": [
{
"token": "example",
"name": "Ada Lovelace",
"kind": "tickets",
"id": 1,
"family": "ticket_type"
}
]
}
The DesignToken object
One control in the studio: what it is, what it may hold, and what it holds now in each state.
Attributes
keystringkindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
The AccountDesign object
The account's design: the tokens every event inherits, in both states. Every token is listed, set or not — the list IS the vocabulary, so one answer says both what may be written and what is.
Attributes
draft_openboolean- Whether anything is staged. True from the first edit, false again on publish or discard.
tokensarray of DesignTokentokens[].keystringtokens[].kindstring- What a value may be:
color(a hex),url,image(a URL, an empty string for "no image", or anasset:<id>the upload path mints),radius(0–40),choice(one ofoptions),bool("1"/"0", and a write also takes "true"/"false", stored as "1"/"0"),rows,text,faces,site_headerorsite_footer. The last two hold JSON as a string: aFrameHeaderand aFrameFooter, the header and footer Customize’s Frame tab sets. An empty value says nothing at this scope, so an event takes the account’s, and the account takes its look’s ownsite_headerandsite_footer. tokens[].optionsarray of string- The closed value set for a
choicetoken, empty for every other kind.look_basealso admitsbase:<id>— an account invents its own bases — and nothing else; itsclassic, the old Classic look, is the Classic starting theme now, so it is never written, and a design still holding it is drawn as Tito's own page.date_formatalso admitslong,shortandweekday, formats it no longer offers: a design holding one keeps it, and the event's date line reads as it always did. Itscustomdraws the design'sdate_pattern, atexttoken of up to 64 characters in which d, m and y (in either case, one to four of each) stand for the day, month and year, and anything in double quotes or square brackets is printed as written. A pattern with no d, m or y outside quotes is refused.banner_alt, the event banner picture's alt text, is atexttoken of up to 250 characters on one line, set on an event's design: blank leaves the picture decorative. Somechoicetokens hold an ordered list rather than one of a set, so theiroptionsare empty and the value is checked by its shape:block_orderis the event page's blocks in order,order_page_orderis the order page's main column in order (every one ofitems,details,tickets,qrandchange, once each, as a comma list),ticket_page_orderis what follows the ticket on its page (scheduleanddetails, in either order), andprint_extrasis what the printed ticket prints after the ticket type and name, in print order, as a comma list offield:<id>(an attendee field or a question),session:<id>(a session offering) andgood:<id>(an add-on). It is set on an event's design only, and every id must be one the event can print: an attendee field its attendees can see, a session offering or add-on of this event, or a question asked about each attendee (not a tickbox). Any other id is refused with a 422 naming it. An item the event can no longer print later, a deleted field or an archived offering, is left off the ticket. tokens[].draftstring- This token's staged value, empty where the scope sets nothing and the layer below decides.
{
"draft_open": true,
"tokens": [
{
"key": "background",
"kind": "color",
"group": "pro",
"options": [
"example"
],
"draft": "example",
"published": "example"
}
]
}
The AccountDesignInput object
What PATCH /design takes. The whole body is checked before anything is written, so a request that names one bad token writes none of the good ones.
Attributes
tokensobject- Values, keyed as
GET /designlists them. Each is validated by its kind, in the same words the studio would use. Abooltoken takes "1" or "0", or "true" or "false", which are stored as "1" and "0". resetarray of string- Tokens to take back to the layer below — the look's, or the product default. A list of its own rather than a null in
tokens, because for theurlandimagekinds an empty string is a REAL value ("no image"), so clearing and unsetting are two different acts.
{
"tokens": {
"background": "#101418",
"time_format": "12h"
},
"reset": [
"logo_url"
]
}
The CustomPage object
Attributes
{
"id": 1,
"path": "example",
"title": "example",
"body": "example",
"published": true,
"version": "example",
"admin_url": "https://example.com/hooks/tito",
"public_url": "https://example.com/hooks/tito",
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z"
}
The ThemeFile object
Attributes
{
"path": "example",
"size": 1,
"version": "example",
"body": "example",
"body_base64": "example",
"admin_url": "https://example.com/hooks/tito",
"missing_purchase_form": true
}
The ThemeField object
Attributes
{
"key": "example",
"label": "example",
"kind": "ticket",
"description": "example",
"path": "example",
"state": "example",
"options": [
"example"
],
"line": 1,
"field_id": 1
}
The DesignStylesheet object
Attributes
draft_openbooleandraftstringpublishedstringdraft_blockedarray of string- Each forbidden thing the draft uses, named once in the order it first appears: a function as
url(), an at-rule as@import, a property off the allow-list by its name. These are blocked on the root domain, where anything that loads from another address is refused; the account's own address serves the stylesheet as written. Empty when nothing is blocked.
{
"draft_open": true,
"draft": "example",
"published": "example",
"draft_blocked": [
"example"
],
"published_blocked": [
"example"
]
}
The DesignStylesheetInput object
Attributes
{
"css": "example"
}
The DesignWording object
Attributes
localestringaudiencestring- The audience slug whose own wording
draftandpublishedcarry, or an empty string for everyone's wording. termsinteger- The id of the payment terms whose own wording
draftandpublishedcarry, or 0 for everyone's wording. Never set beside a non-emptyaudience. draft_openbooleanwordsarray of DesignWordingRowwords[].keystringwords[].screensarray of stringwords[].formsarray of stringwords[].hideablebooleanwords[].formattableboolean- The line may carry bold, italic, bullets and links. Once formatted, its draft and published values are a composer body rather than a plain sentence.
words[].per_audienceboolean- An audience of the event can have its own wording for this line, on top of everyone's. Only these lines are listed with
?audience=, and only these acceptaudiencein a change. words[].per_termsboolean- A set of payment terms can have its own wording for this line, on top of everyone's: the lines of the invoice emails. Only these lines are listed with
?terms=, and only these accepttermsin a change. No line is bothper_audienceandper_terms. words[].draftobjectwords[].publishedobjectwords[].argsarray of string
{
"locale": "example",
"audience": "example",
"terms": 1,
"draft_open": true,
"words": [
{
"key": "example",
"screens": [
"example"
],
"forms": [
"example"
],
"hideable": true,
"formattable": true,
"per_audience": true,
"per_terms": true,
"draft": {},
"published": {},
"draft_hidden": true,
"published_hidden": true,
"args": [
"example"
]
}
]
}
The DesignWordingRow object
Attributes
keystringscreensarray of stringformsarray of stringhideablebooleanformattableboolean- The line may carry bold, italic, bullets and links. Once formatted, its draft and published values are a composer body rather than a plain sentence.
per_audienceboolean- An audience of the event can have its own wording for this line, on top of everyone's. Only these lines are listed with
?audience=, and only these acceptaudiencein a change. per_termsboolean- A set of payment terms can have its own wording for this line, on top of everyone's: the lines of the invoice emails. Only these lines are listed with
?terms=, and only these accepttermsin a change. No line is bothper_audienceandper_terms. draftobjectpublishedobjectargsarray of string
{
"key": "example",
"screens": [
"example"
],
"forms": [
"example"
],
"hideable": true,
"formattable": true,
"per_audience": true,
"per_terms": true,
"draft": {},
"published": {},
"draft_hidden": true,
"published_hidden": true,
"args": [
"example"
]
}
The DesignWordingInput object
Attributes
localestringkeystringformstringvaluestringaudiencestring- An event's audience slug: changes that audience's own wording for the line instead of everyone's. The line must be one with
per_audiencetrue. Not accepted for account wording, and not withterms. termsinteger- The id of a set of payment terms (
GET /payment-terms): changes those terms' own wording for the line instead of everyone's. The line must be one withper_termstrue. Not accepted for account wording, and not withaudience.
{
"locale": "example",
"key": "example",
"form": "example",
"value": "example",
"audience": "example",
"terms": 1
}
{
"key": "example",
"hidden": true
}