# Quickstart

This guide takes you end-to-end: you'll create an event, add a ticket tier,
register a customer, take a card payment, issue a ticket, fetch its QR pass, and
check it in at the door. Every step is a single REST call you can copy and paste.

> **Note:** Run the whole tutorial with a **test key** (`sk_test_…`). Test mode hits the same
> code paths as production but is fully isolated from your live data. See
> [Test & live modes](#test-and-live-modes).


**Base URL** for every call:

```text
https://api.ticketconnect.example/v1
```

**Conventions used throughout:**

* Authenticate with `Authorization: Bearer sk_test_…`.
* Money is always in your account's currency (fiat) and is computed
  server-side — you never send an amount.
* Mutating `POST`s accept an optional `Idempotency-Key` header so retries are
  safe.
* Errors come back as `{ "error": { "type", "code", "message", "param", "request_id" } }`.

---

## Step 1 — Get your API key

Create a secret key in your dashboard. You'll get a key that starts with
`sk_test_` (sandbox) or `sk_live_` (production). The key is shown **once** at
creation, so store it somewhere safe.

For this tutorial you only need a test key. For the full breakdown of key
formats, the `Authorization: Bearer` transport, and the scopes each call
requires, read [Authentication](#authentication).

> **Warning:** Treat your secret key like a password. Never commit it to source control or ship
> it in client-side code.


---

## Step 2 — Confirm your key

A quick way to verify your key works and see which mode it's in.

**Endpoint:** `GET /v1/account` — requires a valid API key (no special scope).

**cURL**

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

**Node (fetch)**

```javascript
const BASE = "https://api.ticketconnect.example/v1";
const KEY = "sk_test_your_key_here";

const res = await fetch(`${BASE}/account`, {
  headers: { Authorization: `Bearer ${KEY}` },
});
const account = await res.json();
console.log(account);
```

**Example response:**

```json
{
  "id": "ten_8a1f...",
  "name": "Acme Tickets",
  "mode": "test",
  "status": "active",
  "currency": "USD"
}
```

The `mode` field tells you whether this key is `test` or `live`. The `currency`
is your account's settlement currency — every price in this guide is in that
currency.

---

## Step 3 — Create an event

**Endpoint:** `POST /v1/events` — requires the **`events:write`** scope.

Only `name` and `date` are required. The other fields (venue, city, description,
etc.) are optional.

**cURL**

```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 Sounds 2026",
    "date": "2026-08-15T19:00:00Z",
    "venue": "Riverside Arena",
    "city": "Austin",
    "country": "US"
  }'
```

**Node (fetch)**

```javascript
const res = await fetch(`${BASE}/events`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "evt-create-001",
  },
  body: JSON.stringify({
    name: "Summer Sounds 2026",
    date: "2026-08-15T19:00:00Z",
    venue: "Riverside Arena",
    city: "Austin",
    country: "US",
  }),
});
const event = await res.json();
console.log(event.id); // evt_...
```

**Example response (trimmed):**

```json
{
  "id": "evt_3f9c2a7b1e4d...",
  "name": "Summer Sounds 2026",
  "status": "draft",
  "date": "2026-08-15T19:00:00.000Z",
  "venue": "Riverside Arena",
  "city": "Austin",
  "country": "US",
  "currency": "USD",
  "ticketPools": [],
  "created_at": "2026-06-05T12:00:00.000Z"
}
```

Save the `id` (e.g. `evt_3f9c2a7b1e4d...`) — you'll use it as `:id` in the next
steps.

> **Note:** A new event starts in `draft` status with no ticket tiers yet. The `date` field
> accepts any value the standard date parser understands; an ISO 8601 timestamp is
> the safest choice.


---

## Step 4 — Add a ticket tier

A tier defines a name, a fiat price, and how many tickets are available.

**Endpoint:** `POST /v1/events/:id/tiers` — requires the **`events:write`** scope.

Required fields: `name` (string), `price` (a non-negative number in your
currency), and `totalSupply` (a positive integer). You may also send `perks`,
`upgradesEnabled`, and advanced sale options (a published `price_steps`
schedule, `release_at` / `release_after_sold_out` auto-release) — see the
[Events reference](#api-events).

**cURL**

```bash
curl https://api.ticketconnect.example/v1/events/evt_3f9c2a7b1e4d.../tiers \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "General Admission",
    "price": 49.00,
    "totalSupply": 500,
    "upgradesEnabled": true
  }'
```

**Node (fetch)**

```javascript
const eventId = "evt_3f9c2a7b1e4d...";

const res = await fetch(`${BASE}/events/${eventId}/tiers`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "General Admission",
    price: 49.0,
    totalSupply: 500,
    upgradesEnabled: true,
  }),
});
const event = await res.json();
console.log(event.ticketPools);
```

**Example response (trimmed):**

```json
{
  "id": "evt_3f9c2a7b1e4d...",
  "name": "Summer Sounds 2026",
  "status": "draft",
  "currency": "USD",
  "ticketPools": [
    {
      "name": "General Admission",
      "price": 49,
      "totalSupply": 500,
      "issued": 0,
      "upgradesEnabled": true
    }
  ],
  "created_at": "2026-06-05T12:00:00.000Z"
}
```

The response is the full updated event. The tier you just added appears in
`ticketPools` with `issued: 0`. You'll reference the tier later by its `name`
(here, `"General Admission"`).

---

## Step 5 — Create a customer

A customer is just an email and a name, plus your own `externalId` so you can
match the customer back to your system.

**Endpoint:** `POST /v1/customers` — requires the **`customers:write`** scope.

Required fields: `externalId` (your own id for this customer) and `email`. `name`
is optional.

**cURL**

```bash
curl https://api.ticketconnect.example/v1/customers \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "user_42",
    "email": "fan@example.com",
    "name": "Jordan Rivera"
  }'
```

**Node (fetch)**

```javascript
const res = await fetch(`${BASE}/customers`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    externalId: "user_42",
    email: "fan@example.com",
    name: "Jordan Rivera",
  }),
});
const customer = await res.json();
console.log(customer.id); // "user_42"
```

**Example response:**

```json
{
  "id": "user_42",
  "email": "fan@example.com",
  "name": "Jordan Rivera",
  "status": "active",
  "created_at": "2026-06-05T12:01:00.000Z"
}
```

The customer's `id` is the `externalId` you supplied — use it as `customer_id`
when issuing the ticket. The first time you reference a new customer the platform
sets everything up behind the scenes, so the response is the same whether the
customer is brand new or already on file.

---

## Step 6 — Take payment

Because the tier above is priced above zero, the ticket must be paid for before
it can be issued. Create a payment intent for the event and tier; the amount is
computed **server-side** from the tier price, so you never send an amount.

**Endpoint:** `POST /v1/payment_intents` — requires the **`payments:write`** scope.

Required fields: `event_id` and `tier`. You may also pass `customer_id`.

**cURL**

```bash
curl https://api.ticketconnect.example/v1/payment_intents \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pi-summer-001" \
  -d '{
    "event_id": "evt_3f9c2a7b1e4d...",
    "tier": "General Admission",
    "customer_id": "user_42"
  }'
```

**Node (fetch)**

```javascript
const res = await fetch(`${BASE}/payment_intents`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "pi-summer-001",
  },
  body: JSON.stringify({
    event_id: "evt_3f9c2a7b1e4d...",
    tier: "General Admission",
    customer_id: "user_42",
  }),
});
const intent = await res.json();
console.log(intent.id, intent.status);
```

**Example response (trimmed):**

```json
{
  "object": "payment_intent",
  "id": "pi_1QabcDEF...",
  "amount": 49,
  "currency": "USD",
  "status": "requires_payment",
  "client_payload": { "client_secret": "pi_1QabcDEF..._secret_..." }
}
```

You collect the card payment on your side using `client_payload` (for example,
with the standard payment SDK in your frontend). Once the payment succeeds,
you'll pass the intent's `id` to the issuance call in the next step.

> **Note:** In test mode, use the standard test card numbers to simulate a successful
> payment — no real money moves. See [Test & live modes](#test-and-live-modes).


---

## Step 7 — Issue the ticket

With a verified payment in hand, issue the ticket to your customer.

**Endpoint:** `POST /v1/events/:id/tickets` — requires the **`tickets:issue`** scope.

Required fields: `customer_id`, `tier`, and — for any paid tier —
`payment_intent_id`. The platform re-verifies the payment, checks it matches this
event and amount, and guards against using the same payment twice.

> **High-demand on-sale?** If the event uses the on-sale waiting room (its
> `queue` field is non-null), the customer must be **admitted by the queue**
> before this call succeeds — otherwise it returns a `403` with a `queue_error`
> (`not_admitted`). Join and poll with `POST /v1/events/:id/queue/join` and
> `GET /v1/events/:id/queue?customer_id=…` (scope **`tickets:issue`**), then
> issue once `status` is `admitted`. Events without a queue are unaffected.


**cURL**

```bash
curl https://api.ticketconnect.example/v1/events/evt_3f9c2a7b1e4d.../tickets \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: issue-summer-001" \
  -d '{
    "customer_id": "user_42",
    "tier": "General Admission",
    "payment_intent_id": "pi_1QabcDEF..."
  }'
```

**Node (fetch)**

```javascript
const eventId = "evt_3f9c2a7b1e4d...";

const res = await fetch(`${BASE}/events/${eventId}/tickets`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "issue-summer-001",
  },
  body: JSON.stringify({
    customer_id: "user_42",
    tier: "General Admission",
    payment_intent_id: "pi_1QabcDEF...",
  }),
});
const ticket = await res.json();
console.log(ticket.id); // tkt_...
```

**Example response (trimmed):**

```json
{
  "id": "tkt_5b8e1c2d3f4a...",
  "event_id": "evt_3f9c2a7b1e4d...",
  "event_name": "Summer Sounds 2026",
  "status": "valid",
  "tier": "General Admission",
  "price": 49,
  "currency": "USD",
  "qr": {
    "data": "a1b2c3d4e5f6...",
    "format": "QR"
  },
  "created_at": "2026-06-05T12:02:00.000Z"
}
```

Every ticket is tamper-proof and anti-scalping is on by default — all handled
for you (and configurable per event; see
[Marketplace and resale](#marketplace-and-resale)). The `qr.data` string is the
opaque code your scanner reads at the door.

> **Tip:** The same data is also delivered to your webhook as a **`ticket.issued`** event,
> so your systems can react in real time. See the
> [Webhooks guide](#webhooks).


---

## Step 8 — Get the ticket's QR / pass

You usually already have the ticket from Step 7, but you can fetch it any time to
get its current status and delivery data.

**Endpoint:** `GET /v1/tickets/:id` — requires the **`tickets:read`** scope.

**cURL**

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

**Node (fetch)**

```javascript
const ticketId = "tkt_5b8e1c2d3f4a...";

const res = await fetch(`${BASE}/tickets/${ticketId}`, {
  headers: { Authorization: `Bearer ${KEY}` },
});
const ticket = await res.json();
console.log(ticket.qr.data);
```

**Example response (trimmed):**

```json
{
  "id": "tkt_5b8e1c2d3f4a...",
  "event_name": "Summer Sounds 2026",
  "status": "valid",
  "tier": "General Admission",
  "qr": {
    "data": "a1b2c3d4e5f6...",
    "format": "QR"
  },
  "created_at": "2026-06-05T12:02:00.000Z"
}
```

Render `qr.data` as a QR code and deliver it through your own app, email, or
ticket page. No internal identifiers are ever returned.

---

## Step 9 — Check in at the door

When the attendee arrives, validate the QR they present. Validation returns a
verdict without consuming the ticket.

**Endpoint:** `POST /v1/scan/validate` — requires the **`scanning:write`** scope.

Required field: `qr` (the `qr.data` value from the ticket).

**cURL**

```bash
curl https://api.ticketconnect.example/v1/scan/validate \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "qr": "a1b2c3d4e5f6..." }'
```

**Node (fetch)**

```javascript
const res = await fetch(`${BASE}/scan/validate`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ qr: "a1b2c3d4e5f6..." }),
});
const verdict = await res.json();
console.log(verdict.valid); // true
```

**Example response (valid ticket):**

```json
{
  "valid": true,
  "status": "valid",
  "tier": "General Admission",
  "ticket_id": "tkt_5b8e1c2d3f4a..."
}
```

If the ticket has already been used or refunded, `valid` is `false` and `reason`
explains why (for example `"already_used"` or `"refunded"`).

> **Record the actual check-in.** `POST /v1/scan/validate` only checks validity. To
> mark a ticket as attended, call **`POST /v1/tickets/:id/attendance`** (also
> **`scanning:write`**) — it's safe to retry and reports `already_used` if the
> ticket was already checked in. For offline scanners draining a queue, use
> **`POST /v1/scan/batch`** to validate up to 200 codes in one call.


---

## Step 10 (optional) — Receive a webhook

Instead of polling, register a webhook endpoint and let TicketConnect push events
to you — `ticket.issued` when a ticket is created, `attendance.verified` on
check-in, and more. Webhooks are signed so you can verify them with existing SDKs.

Manage endpoints with `POST /v1/webhook_endpoints` (requires the
**`webhooks:manage`** scope). See the full walkthrough in the
[Webhooks guide](#webhooks).

---

## You did it

You just sold and scanned a ticket end-to-end:

1. Confirmed your key with `GET /v1/account`.
2. Created an event and a ticket tier.
3. Registered a customer.
4. Took a card payment and issued a tamper-proof ticket.
5. Fetched its QR pass and validated it at the door.

> **Next steps**
>
> * [Authentication](#authentication) — keys, the `Bearer` transport, and the full scopes table.
> * [Test & live modes](#test-and-live-modes) — go live safely after testing in the sandbox.
> * [How it works](#how-it-works) — the mental model behind events, tickets, and payments.
> * [Webhooks guide](#webhooks) — receive real-time, signed events.
> * [API reference](#api-overview) — every endpoint, parameter, and response.
