Skip to content

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/v1

Keys

Keys belong to your account, and you manage them in Account → API. There are two kinds.

Publishablestage_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.
Secretstage_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:

Request
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.

URL
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 }

The account the key belongs to, and the key’s kind. A cheap way to check a key works.
Response
{
  "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 }

Your published events. Tours are listed as their dates, each with tour.id.
when
upcoming (the default): not yet ended, soonest first. past: ended, newest first. all: both, soonest first.
limit, cursor
See pagination.
Response
{
  "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 }

One published event by its id or its slug, with its ticket types, their prices and what each has left.

GET /events/:event/attendees

Secret key

{ attendees: Ticket[], nextCursor }

One event’s tickets, with their buyers and check-in times, newest first. Draft events count too: a draft can still hold tickets.
status
Only tickets with this status.
limit, cursor
See pagination.

GET /tickets

Secret key

{ tickets: Ticket[], nextCursor }

Every ticket on your account, newest first.
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.
Example
// 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.

Example
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.

Response
{
  "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.

Event list
<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>