# Perks

Perks are event add-ons — a welcome drink, lounge access, a backstage meet —
that become **redeemable grants** a customer presents at the door. You define a
perk on an event, grants are issued (now, to named customers or a section, or
automatically as tickets in a tier sell), and your scanner redeems each grant by
its opaque `code`.

A grant's `code` is the only handle you ever see or pass around — there are no
wallets, signatures, or on-chain concepts in these responses.

A serialized perk definition:

```json
{
  "id": "def_9f2c1a7b",
  "event_id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
  "name": "Welcome drink",
  "redeemable": true,
  "max_uses": 1,
  "distribution": "manual",
  "active": true,
  "created_at": "2026-07-11T12:00:00.000Z"
}
```

## Distribution

| Mode | How grants are handed out |
| --- | --- |
| `tier` | Auto-granted to a holder the moment they're issued a ticket in the named `tier` (including comps). Nothing to trigger — define it once. |
| `manual` | Issued immediately to `recipients`: every current holder (`all`), a `section`, or named `customers`. |

`loyalty` distribution (airdrop by attendance history) is a first-party-only
channel and returns `400 not_supported` on this API.

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

Create a perk definition for one of your events. **Scope:** `events:write`.

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | Display name of the perk. |
| `description` | string | No | Longer description shown to staff/holder. |
| `distribution` | string | Yes | `tier` or `manual`. |
| `redeemable` | boolean | No | Whether the perk is redeemed at the door (default `true`). A non-redeemable perk is informational only and issues no grants. |
| `max_uses` | integer \| null | No | Redemptions per grant (positive integer, or `null` for unlimited). Default `1`. |
| `tier` | string | Conditional | Required when `distribution` is `tier` — the ticket tier that earns the perk. |
| `section` | string | No | Optional section qualifier. |
| `scan_station` | string | No | Hint for your scanner UI (e.g. `bar`, `backstage`). |
| `recipients` | object | Conditional | Required when `distribution` is `manual`: `{ "type": "all" }`, `{ "type": "section", "section": "VIP" }`, or `{ "type": "customers", "customer_ids": ["cus_1", ...] }` (1–500 ids). |

For a redeemable `manual` perk the response also carries `issued`, `skipped`,
and `recipients` counts. `tier` perks issue no grants now — they materialize as
matching tickets sell.

### Example request

```bash
curl https://api.ticketconnect.example/v1/events/evt_a1b2c3d4e5f6a7b8c9d0e1f2/perks \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Welcome drink",
    "distribution": "manual",
    "redeemable": true,
    "max_uses": 1,
    "recipients": { "type": "customers", "customer_ids": ["cus_9f2c1a7b"] }
  }'
```

> Errors: `400 parameter_missing` (no `name`/`recipients`), `400 parameter_invalid`
> (bad `distribution`/`max_uses`/`tier`), `400 not_supported` (`distribution: "loyalty"`),
> `404 resource_missing` (not your event).

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

List the event's perk definitions with grant rollups. **Scope:** `events:read`.
Each row adds `grants_issued` and `grants_redeemed`.

## GET /v1/customers/{id}/perks

List a customer's perk grants. **Scope:** `customers:write`. Each grant carries
the opaque `code` your scanner reads, plus `perk_name`, `status`, and
`uses_remaining` (`null` = unlimited).

```json
{
  "object": "list",
  "data": [
    {
      "code": "grant_3f7a9c0d1e2f",
      "perk_name": "Welcome drink",
      "event_id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
      "status": "active",
      "uses_remaining": 1
    }
  ],
  "has_more": false
}
```

## POST /v1/perks/redeem

Redeem a grant by its `code` at the door. **Scope:** `scanning:write`. Online
and atomic — a single-use grant is consumed exactly once; an unlimited grant
stays valid and each scan logs.

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | string | Yes | The grant `code` from the customer. |

### Example response

```json
{
  "valid": true,
  "perk": { "name": "Welcome drink", "event_id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2" },
  "uses_remaining": 0
}
```

A verdict is always returned with `200`. `valid` is `false` with a `reason` of
`already_redeemed`, `revoked`, `expired`, or `not_found` — an unknown code, or a
code belonging to another account, is reported as `not_found` and never
distinguished.
