# Deliver tickets

Once a ticket is issued, you need to put it somewhere the customer can use it at
the door. Every issued ticket carries an opaque **QR string** — the delivery
contract — that you render as a QR code wherever fits your product: your own
app, an email, a printed PDF.

Use this guide after you've issued a ticket (see
[Issue a ticket](#issue-a-ticket)). Everything here reads from a single
endpoint: `GET /v1/tickets/:id`.

## The delivery options

| Option | What you get | When to use it |
| --- | --- | --- |
| **Raw QR string** | `qr.data` — an opaque scannable string | You render the code yourself, or embed it in your own app. The **guaranteed minimum** — always present. |
| **Hosted ticket page** | A signed, expiring link (`POST /v1/tickets/:id/delivery_link`) to a mobile page with the event, holder, live status badge, and the QR | You want a ready-made page to email, text, or hand to the customer — zero front-end work, always shows the ticket's current state. |
| **Hosted checkout** | The branded checkout page shows the buyer their QR on-screen right after purchase | You sell through the hosted checkout (`GET /v1/checkout/:eventId?key=pk_...`) and want zero front-end work. |

The raw QR string is always available the moment a ticket is issued, and it is
the same code the hosted ticket page and the hosted checkout display.

## Before you start

You'll need:

* An API key with the **`tickets:read`** scope.
* An issued ticket id (`tkt_...`).

Base URL:

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

## Step 1 — Fetch the ticket

**Endpoint:** `GET /v1/tickets/:id` · **Scope:** `tickets:read`

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

```json
{
  "id": "tkt_4f8a9c0d1e2f",
  "event_id": "evt_summerfest",
  "event_name": "Summer Fest 2026",
  "status": "valid",
  "tier": "General Admission",
  "qr": {
    "data": "8f2c1a...e7",
    "format": "QR"
  },
  "created_at": "2026-06-05T14:22:00.000Z"
}
```

The `qr.data` field is your raw QR string, and `qr.format` tells you how to
render it (e.g. `QR`).

## Step 2 — Present the ticket to your customer

### Option A — Render the QR string yourself

Take `qr.data` and render it as a QR code with any standard library in your stack,
then show it in your app, your email, or a printed PDF. This is the most flexible
option and works everywhere.

> **`qr.data` is an opaque string.** It's a random value that uniquely identifies
> the ticket for scanning — it carries no personal or sensitive data, and it isn't
> a price, a customer id, or anything you should parse. Just render it.


### Option B — Let the hosted checkout show it

If the ticket was bought through the hosted checkout page
(`GET /v1/checkout/:eventId?key=pk_...`), the buyer sees the rendered QR code
on-screen the moment the purchase completes — no front-end work on your side.
It's the same `qr.data` you can fetch later with `GET /v1/tickets/:id`, so you
can re-deliver it through your own channels at any time.

### Option C — Send a hosted ticket page link

Mint a signed link to a ready-made mobile ticket page and deliver *that* —
in an email, an SMS, or a "view your ticket" button:

```bash
curl https://api.ticketconnect.example/v1/tickets/tkt_4f8a9c0d1e2f/delivery_link \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "expires_in_days": 30 }'
```

```json
{
  "object": "delivery_link",
  "url": "https://api.ticketconnect.example/v1/t/dGt0XzRmOGE5YzBkLi4u...",
  "expires_at": "2026-08-10T14:22:00.000Z"
}
```

> **Links are bound to the QR code they were minted for.** A transfer rotates
> the ticket's code, so every previously minted delivery link stops working —
> the old holder's link can never show the new holder's code. Mint a fresh
> link after a transfer. Revoking the signing secret (`TICKET_LINK_SECRET`)
> invalidates all outstanding links at once.

The page shows your logo, the event details, the holder's name and masked
email, a live status badge, and the rendered entry QR — self-contained, mobile
first, no login. It is **live**: a refund or revocation flips the badge the
moment it happens, so the page is always truthful even while the link stays
valid. Links expire after `expires_in_days` (1–90, default 30); expired or
tampered links all render one identical 404 page. Minting needs only the
`tickets:read` scope. See the
[API reference](#api-tickets) for the full contract.

> **No pass file through this API.** `/v1` does not expose an Apple/Google
> pass file for a ticket — the hosted ticket page is the ready-made delivery
> surface, and `qr.data` remains the delivery contract for everything you
> build yourself. Holders who keep the ticket in a TicketConnect ticket
> wallet **can** save it to Apple/Google Wallet from there — but only for
> events with `live_codes` off: a wallet pass is a static barcode, and the
> platform refuses one where a live, refreshing code is enforced (that
> refusal is what makes the anti-screenshot layer real).

## At the door

Whichever way the customer receives it, your scanners validate it through the
same endpoints. The ticket's `status` reflects its lifecycle — a freshly issued
ticket is `valid`, and it becomes `used` after check-in. A refunded ticket
surfaces as `refunded` here (a transfer keeps the ticket `valid` — same ticket,
new owner), and a revoked ticket keeps its `status` but carries `revoked: true`
and fails every scan. A quick `GET /v1/tickets/:id` always tells you whether a
ticket is good to admit.

> **Holders may also present the ticket from a TicketConnect ticket wallet.**
> A customer who keeps their ticket in a TicketConnect-powered wallet (the
> TicketConnect app, or any holder experience built on the same rails) shows a
> **live code that refreshes every few seconds** instead of your static one.
> Your scanning endpoints validate both — send whatever was scanned, verbatim,
> and read the verdict (see
> [Scanning &amp; check-in](#scanning-and-check-in)). Your `qr.data` stays
> valid alongside it. One consequence to know about: once the holder has
> opened the ticket in a wallet, the ticket is **locked to them** —
> `POST /v1/tickets/:id/transfer` then returns `409 ticket_not_transferable`.
> Live codes are on by default and togglable per event (`live_codes` on the
> [events API](#api-events)).

## See also

* [Issue a ticket](#issue-a-ticket) — create the ticket you're delivering.
* [Transfer, refund &amp; upgrade](#transfer-refund-upgrade) — what happens to delivery after a lifecycle change.
* [API reference: retrieve a ticket](#api-tickets)
