# Events

Events are the things you sell tickets to. Each event holds one or more **ticket
tiers** (name, price, supply, perks). **Organizations** are read-only
sub-accounts you can list and retrieve alongside your events.

A serialized event object looks like this:

```json
{
  "id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
  "name": "Summer Festival 2026",
  "status": "draft",
  "date": "2026-08-15T18:00:00.000Z",
  "venue": "Riverside Park",
  "city": "Austin",
  "country": "US",
  "category": "music",
  "description": "Two stages, twenty acts.",
  "currency": "USD",
  "imageUrl": "https://acme.example/img/summerfest-card.jpg",
  "bannerUrl": "https://acme.example/img/summerfest-hero.jpg",
  "ticketPools": [
    {
      "name": "General Admission",
      "price": 49.99,
      "totalSupply": 5000,
      "issued": 0,
      "upgradesEnabled": false,
      "perks": null,
      "release": { "released": true }
    }
  ],
  "queue": null,
  "live_codes": true,
  "created_at": "2026-06-05T12:00:00.000Z"
}
```

> The `issued` field on a tier is the number of tickets already issued from that
> tier; `totalSupply - issued` is what remains.

Tier `price` is always the **schedule-effective** price — what checkout charges
right now. A tier that runs a published price schedule additionally carries:

| Field | Type | Description |
| --- | --- | --- |
| `base_price` | number | The tier's base price before scheduled steps. |
| `price_schedule` | array | Every published step: `{ starts_at, price, label?, status }` with `status` one of `past`, `active`, `upcoming`. |
| `next_price_change` | object \| null | The next step, `{ at, price, label? }`, or `null` when no further change is published. |

Every tier carries a `release` object describing its auto-release state:
`{ "released": true }` for tiers on sale, or `{ "released": false }` plus
`release_at` (unlock time) and/or `awaiting_sold_out_of` (the sibling tier that
must sell out first). Locked tiers are visible but cannot be issued from.

`queue` is non-null only when the event uses an on-sale waiting room (see
[Waiting room (queue)](#waiting-room-queue)): `{ "enabled": true,
"sales_start_at": "2026-08-01T10:00:00.000Z" }`.

`live_codes` says whether tickets held in a **TicketConnect ticket wallet**
(the TicketConnect app, or any holder experience built on the same rails)
show a **live, refreshing entry code** for this event (an anti-screenshot
measure — see [Scanning &amp; check-in](#scanning-and-check-in)). It is `true`
by default, settable at creation and togglable with
[`PATCH /v1/events/{id}`](#patch-v1-events-id). It never affects the static
`qr.data` you deliver yourself. It also gates phone passes: a wallet-held
ticket can be saved to Apple/Google Wallet only when live codes are **off**
(a phone pass is a static barcode, so the platform refuses one where a live
code is enforced).

`imageUrl` (card/list image) and `bannerUrl` (wide hero) are the event's two
media slots — set them at creation via `images` or later with
[`PATCH /v1/events/{id}`](#api-events). Events with a published seat map sell
seated tiers by the seat (see [Reserved seating](#api-seating)), and every
event can list its closest catalog neighbours via
[`GET /v1/events/{id}/similar`](#api-similar).

---

## GET /v1/events

List your events, newest first.

**Scope:** `events:read`

### Query params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | No | Page size, `1`–`100` (default `20`). |
| `starting_after` | string | No | Cursor — an event id to page after. |

### Example request

```bash
curl "https://api.ticketconnect.example/v1/events?limit=2" \
  -H "Authorization: Bearer sk_test_your_key_here"
```

### Example response

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    { "id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2", "name": "Summer Festival 2026", "status": "draft", "ticketPools": [] }
  ]
}
```

---

## POST /v1/events

Create an event under your account. Returns `201` with the new event. New
events start in `draft` status with no tiers — add tiers with
[`POST /v1/events/{id}/tiers`](#post-v1-events-id-tiers).

**Scope:** `events:write`

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | Event name. |
| `date` | string \| number | Yes | Event start, any value the date parser accepts (ISO 8601 recommended). |
| `venue` | string | No | Venue name. |
| `location` | string | No | Free-form location/address. |
| `city` | string | No | City. |
| `country` | string | No | Country code. |
| `category` | string | No | Category, e.g. `music`. |
| `description` | string | No | Event description. |
| `currency` | string | No | Currency for this event; defaults to your account currency. |
| `max_tickets_per_identity` | integer | No | Per-person purchase cap for this event, `1`–`50`. The platform default applies when unset. |
| `images` | object | No | Event media: `{ "image_url": "...", "banner_url": "..." }`. Each must be an `https://` URL of at most 2048 characters (`http://` and `data:` URIs are rejected). There is no gallery — `images.gallery_urls` returns `400 parameter_unknown`. |
| `marketplace_settings` | object | No | Per-event resale / anti-scalping policy (see below). Platform defaults apply when unset. |
| `live_codes` | boolean | No | Whether wallet-held tickets show a live, refreshing entry code (default `true`). Your own `qr.data` delivery is unaffected. |

Accepts an `Idempotency-Key` header.

The `marketplace_settings` object (all fields optional; supplied keys override the platform defaults):

| Field | Type | Description |
| --- | --- | --- |
| `resale_enabled` | boolean | Master switch for the secondary market. Default `true`. When `false`, tickets for this event can't be relisted at all. |
| `anti_scalping_enabled` | boolean | Enforce the resale price floor/ceiling. Default `true`. When `false`, resale price is uncapped. |
| `max_resale_price_percent` | integer | Resale ceiling as % of face value, `100`–`1000`. Default `150`. |
| `min_resale_price_percent` | integer | Resale floor as % of face value, `0`–`100`. Default `0`. |
| `resale_start_mode` | string | When resale opens: `immediate`, `hours_before_event`, or `days_before_event`. Default `immediate`. |
| `resale_start_value` | integer | Hours or days before the event, paired with `resale_start_mode`. Default `0`. |

`royalty_rate` is **platform-set per organization** and is not accepted here — sending it returns `400 parameter_unknown`. It appears read-only on the event object so you can see your effective rate.

### Example request

```bash
curl https://api.ticketconnect.example/v1/events \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: evt-create-001" \
  -d '{
    "name": "Summer Festival 2026",
    "date": "2026-08-15T18:00:00Z",
    "venue": "Riverside Park",
    "city": "Austin",
    "country": "US",
    "currency": "USD"
  }'
```

### Example response

```json
{
  "id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
  "name": "Summer Festival 2026",
  "status": "draft",
  "date": "2026-08-15T18:00:00.000Z",
  "currency": "USD",
  "ticketPools": [],
  "queue": null,
  "live_codes": true,
  "created_at": "2026-06-05T12:00:00.000Z"
}
```

---

## GET /v1/events/{id}

Retrieve a single event by id.

**Scope:** `events:read`

### Path params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Event id. |

### Example request

```bash
curl https://api.ticketconnect.example/v1/events/evt_a1b2c3d4e5f6a7b8c9d0e1f2 \
  -H "Authorization: Bearer sk_test_your_key_here"
```

### Example response

```json
{
  "id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
  "name": "Summer Festival 2026",
  "status": "draft",
  "date": "2026-08-15T18:00:00.000Z",
  "ticketPools": [
    { "name": "General Admission", "price": 49.99, "totalSupply": 5000, "issued": 12 }
  ],
  "created_at": "2026-06-05T12:00:00.000Z"
}
```

---

## PATCH /v1/events/{id}

Update an event's **media**, **resale policy** and/or **live-code setting**.
Accepts `images`, `marketplace_settings` and/or `live_codes` — at least one is
required. Partial: only the keys you send change. Pass `null` to unset a media
slot; omitted keys are untouched. Naturally idempotent (no `Idempotency-Key`
needed). Emits an `event.updated` webhook.

**Scope:** `events:write`

### Path params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Event id. |

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `images` | object | Conditional | `{ "image_url": string \| null, "banner_url": string \| null }`. URLs must be `https://`, at most 2048 characters. `null` unsets the slot. |
| `marketplace_settings` | object | Conditional | Same fields as on [create](#post-v1-events) — send only the keys you want to change. |
| `live_codes` | boolean | Conditional | Toggle live, refreshing entry codes for wallet-held tickets. Turning it **off and back on is safe**: codes already delivered to holders' wallets keep working. Turning it **on** for an event whose holders already opened their ticket in a wallet requires them to reopen it online once before the gate. |

### Example request

```bash
curl https://api.ticketconnect.example/v1/events/evt_a1b2c3d4e5f6a7b8c9d0e1f2 \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -X PATCH \
  -d '{
    "images": {
      "image_url": "https://acme.example/img/summerfest-card.jpg",
      "banner_url": null
    }
  }'
```

### Example response

```json
{
  "id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
  "name": "Summer Festival 2026",
  "status": "draft",
  "imageUrl": "https://acme.example/img/summerfest-card.jpg",
  "created_at": "2026-06-05T12:00:00.000Z"
}
```

> Errors: `400 parameter_missing` (none of `images`, `marketplace_settings`,
> `live_codes`), `400 parameter_invalid` (bad media URL, out-of-range resale
> bound, or non-boolean `live_codes`),
> `400 parameter_unknown` (`images.gallery_urls` or `marketplace_settings.royalty_rate`),
> `404 resource_missing` (not your event).

---

## POST /v1/events/{id}/tiers

Add a ticket tier to an event. Returns the full updated event with the new tier
appended to `ticketPools`. The tier's `issued` count always starts at `0`.

**Scope:** `events:write`

### Path params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Event id. |

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | Tier name, e.g. `VIP`. Used as the tier identifier when issuing. |
| `price` | number | Yes | Tier price in the event currency. Must be `>= 0`. |
| `totalSupply` | integer | Yes | Number of tickets available. Must be a positive integer. |
| `perks` | object | No | Free-form perks map, e.g. `{ "lounge": true }`. |
| `upgradesEnabled` | boolean | No | Whether tickets may be upgraded **into** this tier. Defaults to `false`. |
| `price_steps` | array | No | Published price schedule: `[{ "starts_at": "<ISO 8601>", "price": 39.99, "label": "Early bird" }]`. Steps are validated against the event's other tiers; the tier's effective price then follows the schedule. |
| `release_at` | string | No | Keep the tier locked (visible but not purchasable) until this time. |
| `release_after_sold_out` | string | No | Keep the tier locked until the named sibling tier sells out. May be combined with `release_at`. |

Accepts an `Idempotency-Key` header.

> Errors: `404 resource_missing` if the event does not exist or is not yours;
> `400 parameter_invalid` if the schedule or release configuration is invalid
> (e.g. a `release_after_sold_out` cycle or an unknown sibling tier).

### Example request

```bash
curl https://api.ticketconnect.example/v1/events/evt_a1b2c3d4e5f6a7b8c9d0e1f2/tiers \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "VIP",
    "price": 149.00,
    "totalSupply": 200,
    "perks": { "lounge": true },
    "upgradesEnabled": true
  }'
```

### Example response

```json
{
  "id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
  "name": "Summer Festival 2026",
  "status": "draft",
  "ticketPools": [
    { "name": "General Admission", "price": 49.99, "totalSupply": 5000, "issued": 0, "upgradesEnabled": false },
    { "name": "VIP", "price": 149.00, "totalSupply": 200, "issued": 0, "upgradesEnabled": true, "perks": { "lounge": true } }
  ],
  "created_at": "2026-06-05T12:00:00.000Z"
}
```

---

## GET /v1/organizations

List your organizations — read-only sub-accounts grouped with your events.

**Scope:** `events:read`

### Query params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | No | Page size, `1`–`100` (default `20`). |
| `starting_after` | string | No | Cursor — an organization id to page after. |

### Example request

```bash
curl https://api.ticketconnect.example/v1/organizations \
  -H "Authorization: Bearer sk_test_your_key_here"
```

### Example response

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    { "id": "665f1e2d3c4b5a6d7e8f9a0b", "name": "Acme Live", "status": "active", "created_at": "2026-01-10T09:00:00.000Z" }
  ]
}
```

---

## GET /v1/organizations/{id}

Retrieve one of your organizations by id.

**Scope:** `events:read`

### Path params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Organization id. |

### Example request

```bash
curl https://api.ticketconnect.example/v1/organizations/665f1e2d3c4b5a6d7e8f9a0b \
  -H "Authorization: Bearer sk_test_your_key_here"
```

### Example response

```json
{
  "id": "665f1e2d3c4b5a6d7e8f9a0b",
  "name": "Acme Live",
  "status": "active",
  "created_at": "2026-01-10T09:00:00.000Z"
}
```

---

## Waiting room (queue)

High-demand events can run an **on-sale waiting room**. When an event has one,
its serialized `queue` field is non-null and ticket issuance is gated: a
customer must hold a live admission window before
[`POST /v1/events/{id}/tickets`](#api-tickets) will
succeed (otherwise it returns `403` with code `not_admitted`).

The waiting room is a server-side concept — call these endpoints from your
backend and proxy queue state to your own buyer-facing UI. Identity is your
customer's `externalId`, passed as `customer_id`.

The room moves through three phases:

| Phase | Meaning |
| --- | --- |
| `pre_sale` | Before `sales_start_at`. Customers may pre-join; the pre-sale block is shuffled into a fair random order when the sale starts. |
| `queue_active` | The sale is open and the queue is draining. Customers are admitted in batches, each with a short access window. |
| `closed` | The queue has fully drained (or the room is off). Purchases proceed without queueing. |

A customer's queue `status` is `waiting`, `admitted`, or `completed` (bought at
least once), or `null` when they are not in the queue.

> **Polling is the contract.** Poll `GET /v1/events/{id}/queue?customer_id=…`
> every few seconds during the `pre_sale` and `queue_active` phases. A `status`
> of `admitted` with a non-null, future `access_expires_at` is the signal to open
> checkout. No webhook is emitted for admission today.


---

## GET /v1/events/{id}/queue

Return the room phase and aggregate `waiting_count`, plus — when `customer_id`
is supplied — that customer's position, `people_ahead`, admission status, and
access window. Also includes queue-time price transparency: the purchasable
price range and per-tier prices, locked tiers with their unlock conditions, and
the next published price change.

**Scope:** `tickets:issue`

### Path params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Event id. |

### Query params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `customer_id` | string | No | A customer's `externalId`; include it to get that customer's queue entry. |

### Example request

```bash
curl "https://api.ticketconnect.example/v1/events/evt_a1b2c3d4e5f6a7b8c9d0e1f2/queue?customer_id=cus_ext_42" \
  -H "Authorization: Bearer sk_test_your_key_here"
```

### Example response

```json
{
  "event_id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
  "queue_enabled": true,
  "phase": "queue_active",
  "sales_start_at": "2026-08-01T10:00:00.000Z",
  "waiting_count": 1841,
  "position": 212,
  "people_ahead": 187,
  "status": "waiting",
  "access_expires_at": null,
  "pricing": {
    "min_price": 49.99,
    "max_price": 149.00,
    "tiers": [
      { "name": "General Admission", "price": 49.99, "available": 3120, "locked": false },
      { "name": "VIP", "price": 149.00, "available": 0, "locked": true, "awaiting_sold_out_of": "General Admission" }
    ]
  }
}
```

| Field | Type | Description |
| --- | --- | --- |
| `queue_enabled` | boolean | Whether the event currently runs a waiting room. When `false`, `phase` is `closed` and the entry fields are `null`. |
| `phase` | string | `pre_sale`, `queue_active`, or `closed`. |
| `sales_start_at` | string \| null | When the sale opens and the queue starts draining. |
| `waiting_count` | integer | Number of customers currently waiting. |
| `position` | integer \| null | The customer's queue position (only with `customer_id`). |
| `people_ahead` | integer \| null | Waiting customers ahead of this one. |
| `status` | string \| null | `waiting`, `admitted`, or `completed`. |
| `access_expires_at` | string \| null | End of the customer's admission window; non-null only once admitted. |
| `pricing` | object | `min_price`, `max_price`, and per-tier `{ name, price, available, locked, release_at?, awaiting_sold_out_of?, next_price_change? }`. |

> Errors: `404 resource_missing` if the event, or the supplied `customer_id`,
> does not exist.

---

## POST /v1/events/{id}/queue/join

Put a customer in the waiting room. Returns `201` with the customer's entry.
Idempotent — re-joining while holding a live spot returns the existing entry.
Customers who join during `pre_sale` are shuffled into a fair random order at
`sales_start_at`; a customer who joins after the shuffle is appended behind the
randomized block in arrival order.

**Scope:** `tickets:issue`

### Path params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Event id. |

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `customer_id` | string | Yes | The customer's `externalId`. |

### Example request

```bash
curl https://api.ticketconnect.example/v1/events/evt_a1b2c3d4e5f6a7b8c9d0e1f2/queue/join \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "customer_id": "cus_ext_42" }'
```

### Example response

```json
{
  "event_id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
  "customer_id": "cus_ext_42",
  "phase": "queue_active",
  "position": 1854,
  "people_ahead": 1841,
  "status": "waiting",
  "access_expires_at": null
}
```

> Errors: `400 queue_not_enabled` if the event does not use a waiting room;
> `400 queue_join_failed` if the room rejected the join; `404 resource_missing`
> if the event or customer does not exist.

---

## POST /v1/events/{id}/queue/leave

Remove a customer from the queue, giving up any live admission window.
Re-joining afterwards starts at the back of the queue.

**Scope:** `tickets:issue`

### Path params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Event id. |

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `customer_id` | string | Yes | The customer's `externalId`. |

### Example request

```bash
curl https://api.ticketconnect.example/v1/events/evt_a1b2c3d4e5f6a7b8c9d0e1f2/queue/leave \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "customer_id": "cus_ext_42" }'
```

### Example response

```json
{
  "event_id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
  "customer_id": "cus_ext_42",
  "left": true
}
```

> Errors: `404 not_in_queue` if the customer has no active entry;
> `404 resource_missing` if the event or customer does not exist.
