# Scanning

Validate tickets at the door and record check-ins. Validation reads the QR
string from a ticket — either the opaque `qr.data` you delivered, or the live,
refreshing code shown for a ticket held in a TicketConnect ticket wallet
(verified cryptographically on the platform side). Attendance marks a ticket
as used. Your API key with the `scanning:write` scope authorizes scanning —
there is no separate scanner token. See the
[Scanning and check-in](#scanning-and-check-in) guide.

A scan verdict looks like:

```json
{ "valid": true, "status": "valid", "tier": "VIP", "ticket_id": "tkt_4d5e6f..." }
```

When a ticket is not admissible, `valid` is `false` and `reason` is one of
`not_found`, `refunded`, `revoked`, `already_used` — or, for live wallet
codes: `code_expired` (real but stale; have the holder refresh and re-scan),
`live_code_required` (a static copy of a wallet code where the live one is
required), or `invalid_qr` (failed cryptographic verification). Whenever the
ticket was identified, the verdict includes its `ticket_id`; an `already_used`
verdict also carries the `tier` so door staff can see what was scanned.

---

## POST /v1/scan/validate

Validate a single ticket for entry by its QR string. This **does not** mark the
ticket used — call [attendance](#post-v1-tickets-id-attendance) to check in.

**Scope:** `scanning:write`

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `qr` | string | Yes | The QR string read from the ticket, verbatim — your opaque `qr.data` or a live wallet code (a JSON-looking blob; never parse or re-encode it). |

### Example request

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

### Example response

```json
{
  "valid": true,
  "status": "valid",
  "tier": "VIP",
  "ticket_id": "tkt_4d5e6f7a8b9c0d1e2f3a4b5c"
}
```

A failed verdict:

```json
{ "valid": false, "reason": "already_used", "tier": "VIP", "ticket_id": "tkt_4d5e6f7a8b9c0d1e2f3a4b5c" }
```

---

## POST /v1/scan/batch

Validate up to **200** tickets in a single call — ideal for an offline scanner
draining its queue once back online. None are marked used. Returns a per-QR
verdict array.

**Scope:** `scanning:write`

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `qrs` | string[] | Yes | Array of QR strings, verbatim (max 200). Each entry may be either code shape. |

### Example request

```bash
curl https://api.ticketconnect.example/v1/scan/batch \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "qrs": ["a1b2c3...", "d4e5f6...", "unknown-code"] }'
```

### Example response

```json
{
  "results": [
    { "qr": "a1b2c3...", "valid": true, "status": "valid", "tier": "VIP", "ticket_id": "tkt_4d5e6f..." },
    { "qr": "d4e5f6...", "valid": false, "reason": "already_used", "tier": "GA", "ticket_id": "tkt_77aa..." },
    { "qr": "unknown-code", "valid": false, "reason": "not_found" }
  ]
}
```

> Errors: `400 parameter_missing` if `qrs` is not an array; `400 parameter_invalid`
> if it contains more than 200 entries.

---

## POST /v1/tickets/{id}/attendance

Mark a ticket as attended (check-in) by ticket id. This is the action that
actually consumes the ticket. It is safe to retry: an already-used ticket
returns `200` with `status: "already_used"` rather than an error.

**Scope:** `scanning:write`

### Path params

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

This endpoint takes no required body. Accepts an `Idempotency-Key` header.

### Example request

```bash
curl https://api.ticketconnect.example/v1/tickets/tkt_4d5e6f7a8b9c0d1e2f3a4b5c/attendance \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -X POST
```

### Example response

```json
{
  "id": "tkt_4d5e6f7a8b9c0d1e2f3a4b5c",
  "status": "used"
}
```

A repeat call returns:

```json
{
  "id": "tkt_4d5e6f7a8b9c0d1e2f3a4b5c",
  "status": "already_used"
}
```

> Errors: `404 resource_missing` for an unknown ticket. Emits an
> `attendance.verified` webhook on the first successful check-in.
