Developer API
Read your events, ticket types, tickets and attendees as JSON, and build your own event list, calendar or integration. Everything is read-only and belongs to your account.
Base URL
https://api.stage.ooo/v1Keys
Keys belong to your account, and you manage them in Account → API. There are two kinds.
- Publishable
stage_pub_… - Reads your published, public events: what your event pages already show. It is safe to put in a web page. Your account gets one the first time you open Account → API.
- Secret
stage_sec_… - Everything a publishable key reads, plus private events, tickets and attendees. Make one by hand in Account → API. It is shown once, when you make it, so copy it then. Keep it on your server: a request that carries one from a browser is refused.
Allowed websites
A publishable key can be locked to the websites you list, like https://yoursite.com. A browser request from any other website is refused. A key with no websites listed works anywhere. List each origin you use, with its scheme: https://yoursite.com and https://www.yoursite.com are two.
Revoke a key in Account → API and anything using it stops working right away.
Authentication
Send the key as a bearer token:
curl https://api.stage.ooo/v1/events \
-H "Authorization: Bearer stage_pub_…"A publishable key can also go in the query string, for a plain link or an image tag. Secret keys only work in the header.
https://api.stage.ooo/v1/events?key=stage_pub_…Every response is JSON and allows cross-origin requests, so you can call the API straight from a page with a publishable key.
Endpoints
Every endpoint is a GET. Times are ISO 8601 in UTC, amounts are integer cents, and fields are camelCase.
GET /account
{ account, key }
{
"account": {
"id": "8f0c…",
"name": "The Earl",
"slug": "theearl",
"url": "https://stage.ooo/theearl",
"logoUrl": "https://…/logo.png",
"logoColor": "#111111"
},
"key": { "kind": "publishable" }
}GET /events
{ events: Event[], nextCursor }
- when
- upcoming (the default): not yet ended, soonest first. past: ended, newest first. all: both, soonest first.
- limit, cursor
- See pagination.
{
"events": [
{
"id": "2b6e…",
"slug": "winter-market",
"name": "Winter Market",
"subtitle": "Holiday edition",
"url": "https://stage.ooo/theearl/winter-market",
"ticketUrl": "https://stage.ooo/theearl/winter-market",
"startsAt": "2026-12-12T23:00:00.000Z",
"endsAt": "2026-12-13T04:00:00.000Z",
"doorsAt": null,
"timeZone": "America/New_York",
"source": "stage",
"coverImageUrl": "https://…/poster.jpg",
"coverVideoUrl": null,
"venue": {
"name": "The Earl",
"address": "488 Flat Shoals Ave SE",
"city": "Atlanta",
"state": "GA",
"zip": "30316"
},
"tour": null,
"isFree": false,
"soldOut": false,
"price": { "minCents": 1500, "maxCents": 4000, "currency": "usd" }
}
],
"nextCursor": "50"
}GET /events/:event
{ event: EventDetail }
GET /events/:event/attendees
Secret key{ attendees: Ticket[], nextCursor }
- status
- Only tickets with this status.
- limit, cursor
- See pagination.
GET /tickets
Secret key{ tickets: Ticket[], nextCursor }
- event
- Only this event’s tickets, by event id.
- status
- confirmed, pending, payment_pending, approved, denied, refunded or rsvp.
- created_since
- Only tickets made at or after this ISO 8601 time. Poll with it to pick up new sales.
- limit, cursor
- See pagination.
// On your server, with a secret key: the tickets sold since you last asked.
const since = '2026-10-18T00:00:00Z';
const res = await fetch(
`https://api.stage.ooo/v1/tickets?status=confirmed&created_since=${encodeURIComponent(since)}`,
{ headers: { Authorization: `Bearer ${process.env.STAGE_SECRET_KEY}` } },
);
const { tickets, nextCursor } = await res.json();Event
What /events lists. isPrivate is only there for a secret key.
idstring- The event’s id.
slugstring- The event’s address on your account. Unique within your account.
namestring- The event’s name.
subtitlestring | null- The line under the name: a tour date’s city, a subtitle.
urlstring- The event page on Stage.
ticketUrlstring- Where tickets are sold: the Stage page, or the outside site for an event listed from elsewhere. Link here.
startsAtstring | null- When it starts, ISO 8601 in UTC.
endsAtstring | null- When it ends, ISO 8601 in UTC.
doorsAtstring | null- When doors open.
timeZonestring- The IANA time zone the event is on, like America/New_York. Show times on this clock, not the visitor’s.
source"stage" | "external"- stage for an event sold on Stage, external for one listed from another site.
coverImageUrlstring | null- The poster.
coverVideoUrlstring | null- The video poster, when the event has one.
venueobject | null- name, address, city, state and zip; every field but name can be null.
tour{ id } | null- Set when the event is one date of a tour. Dates of the same tour share the id.
isFreeboolean- A free event.
soldOutboolean- Marked sold out. On the event detail, also true when every ticket type is sold out.
priceobject | null- minCents, maxCents and currency ("usd"): the lowest and highest ticket price. Null when the event is free or nothing is priced.
isPrivateboolean- Whether the event is private. Only with a secret key.
Event detail
What /events/:event returns: every Event field, and these.
descriptionstring | null- The event’s description.
ticketTypesTicketType[]- What is on sale, in the order you set.
daysstring[]- A multi-day event’s days, YYYY-MM-DD, ascending. Empty for a single-day event.
Ticket type
idstring- The ticket type’s id.
namestring- Its name, like General admission.
descriptionstring | null- Its description.
priceCentsnumber- Its price in cents.
pricesobject[]- Named price levels within the type (label and priceCents), in the order you set. Empty when it has one price.
availablenumber | null- Places left to sell, or null when the type is not capped. A guide: checkout has the final say.
soldOutboolean- Nothing left of this type, or the event is marked sold out.
Ticket
What /tickets and /events/:event/attendees list. Secret keys only.
idstring- The ticket’s id.
orderIdstring- Tickets bought together share it.
eventIdstring- The event it is for.
statusstring- confirmed, pending, payment_pending, approved, denied, refunded or rsvp.
ticketTypeobject- id and name of its ticket type; either can be null on older tickets.
seatobject | null- label and section, for a reserved seat.
buyerobject- name, email and phone; any can be null.
amountPaidCentsnumber | null- What was paid for this ticket, in cents.
discountCentsnumber | null- The bundle discount on it, in cents.
promoCodeIdstring | null- The promo code used.
paymentProviderstring | null- Who took the payment, like square, stripe or offline.
paymentMethodstring | null- How it was paid.
formResponsesobject | null- Answers to the event’s checkout or application form, keyed by field.
createdAtstring- When the ticket was made.
confirmedAtstring | null- When it was confirmed.
refundedAtstring | null- When it was refunded.
checkedInAtstring | null- The first time it was scanned at the door.
Pagination
Lists come a page at a time. limit sets the page size, from 1 to 100 (50 by default). When there is more, the response’s nextCursor is a string: send it back as cursor for the next page. On the last page it is null.
async function allEvents(key) {
const events = [];
let cursor = null;
do {
const url = new URL('https://api.stage.ooo/v1/events');
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
const body = await res.json();
if (!res.ok) throw new Error(body.error.message);
events.push(...body.events);
cursor = body.nextCursor;
} while (cursor);
return events;
}Errors
An error has an HTTP status and a body with a code you can match on and a message you can show.
{
"error": {
"code": "forbidden",
"message": "This endpoint needs a secret key."
}
}invalid_request400- A parameter is wrong, like when=soon or a status that does not exist. The message says which.
unauthenticated401- No key was sent, or the key is invalid or revoked.
forbidden403- The key cannot do this: a publishable key on a secret endpoint, a publishable key from a website it is not allowed on, or a secret key sent from a browser.
not_found404- No event with that id or slug on your account (or it is not published).
server_error500- Something failed on Stage. Try again.
Example: an event list
Your upcoming events with their posters, dates, venues and prices, each linking to where tickets are sold. Paste it into your site, put your publishable key in, and style it as you like.
<div id="stage-events">Loading events…</div>
<script>
// Your publishable key, from Stage: Account → API.
const STAGE_KEY = 'stage_pub_…';
(async () => {
const list = document.getElementById('stage-events');
const res = await fetch('https://api.stage.ooo/v1/events?when=upcoming&limit=20', {
headers: { Authorization: 'Bearer ' + STAGE_KEY },
});
const body = await res.json();
if (!res.ok) {
list.textContent = body.error.message;
return;
}
if (body.events.length === 0) {
list.textContent = 'No upcoming events.';
return;
}
// Times on the event's own clock, not the visitor's.
const when = (event) =>
new Intl.DateTimeFormat('en-US', {
weekday: 'short', month: 'short', day: 'numeric',
hour: 'numeric', minute: '2-digit',
timeZone: event.timeZone,
}).format(new Date(event.startsAt));
const price = (event) => {
if (event.isFree) return 'Free';
if (!event.price) return '';
const dollars = (cents) => '$' + (cents / 100).toFixed(cents % 100 ? 2 : 0);
return event.price.minCents === event.price.maxCents
? dollars(event.price.minCents)
: dollars(event.price.minCents) + '–' + dollars(event.price.maxCents);
};
list.replaceChildren(...body.events.map((event) => {
const item = document.createElement('a');
item.href = event.ticketUrl;
item.className = 'stage-event';
if (event.coverImageUrl) {
const img = document.createElement('img');
img.src = event.coverImageUrl;
img.alt = '';
img.loading = 'lazy';
item.append(img);
}
const name = document.createElement('strong');
name.textContent = event.name;
const details = document.createElement('span');
details.textContent = [
event.startsAt ? when(event) : null,
event.venue?.name,
event.soldOut ? 'Sold out' : price(event),
].filter(Boolean).join(' · ');
item.append(name, details);
return item;
}));
})();
</script>
<style>
#stage-events { display: grid; gap: 16px; }
.stage-event { display: grid; gap: 4px; color: inherit; text-decoration: none; }
.stage-event img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; border-radius: 8px; }
</style>