# Door staff

Pair a door staff phone with a QR invite, rename or revoke it, and get the check-in report.

The door scanner runs in the phone's browser, so there's no app to install. The phone downloads the ticket list in advance and checks the QR signature itself, so it lets guests in even offline. Check-ins go to the server once the connection is back.

## Pair a phone {#pairing}

1. Create an invite: [`POST /v1/devices/invites`](#invite). Show the door staff member the QR from the response on a screen.
2. They scan the QR with the regular camera. The scanner opens and the phone pairs. No signing in, no password to make up.
3. An invite lives 15 minutes and works once. A paired phone keeps working until someone [revokes](#revoke) it.

A device works in the mode of the key that created the invite: a test device sees test events and tickets only.

## Invite a device {#invite}

`POST /v1/devices/invites`

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no | Entrance name, up to 60 characters. Defaults to «Вход N» (Entrance N) |
| `event_ids` | array | no | Events the device may check. Empty: every event of the organization in the key's mode |

```bash Request
curl https://api.budu.events/v1/devices/invites \
  -H "Authorization: Bearer $BUDU_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Вход 1", "event_ids": ["0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60"]}'
```

```json Response 201
{
  "id": "0193e0e2-5b10-7c20-9d30-4e5f6a7b8c9d",
  "name": "Вход 1",
  "url": "https://budu.events/scan/#i=bi_…",
  "qr_svg": "<svg …>…</svg>",
  "expires_at": "2026-12-12T15:15:00Z",
  "event_ids": ["0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60"]
}
```

`qr_svg` is a ready QR for a screen, `url` is the same link as text. You see them once, the device list doesn't have them. `Idempotency-Key` has no effect here: every call creates a new invite. An event of another organization or mode gives `404`.

## List devices {#list}

`GET /v1/devices`

```bash Request
curl https://api.budu.events/v1/devices -H "Authorization: Bearer $BUDU_KEY"
```

```json Response 200
{
  "devices": [
    {
      "device": {
        "id": "0193e0e2-5b10-7c20-9d30-4e5f6a7b8c9d",
        "name": "Вход 1",
        "org_id": "0193d9a1-2b3c-7d4e-8f50-61728394a5b6",
        "livemode": false,
        "event_ids": ["0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60"]
      },
      "status": "active",
      "created_at_ms": "1797086400000",
      "paired_at_ms": "1797086520000",
      "last_seen_at_ms": "1797098100000",
      "revoked_at_ms": "0"
    }
  ]
}
```

`status`: `pending` (waiting to pair), `active` (paired), `expired` (the invite ran out unused), `revoked` (revoked).

## Rename {#rename}

`PATCH /v1/devices/{id}`

```bash Request
curl -X PATCH https://api.budu.events/v1/devices/0193e0e2-5b10-7c20-9d30-4e5f6a7b8c9d \
  -H "Authorization: Bearer $BUDU_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Главный вход"}'
```

`name` is 1 to 60 characters, no line breaks. You get `200` with the device, as in the list.

## Revoke {#revoke}

`DELETE /v1/devices/{id}`

```bash Request
curl -X DELETE https://api.budu.events/v1/devices/0193e0e2-5b10-7c20-9d30-4e5f6a7b8c9d \
  -H "Authorization: Bearer $BUDU_KEY"
```

You get `204`, and `204` again on a repeat. The phone stops working within 10 seconds: on its next call to the server the scanner wipes the downloaded ticket list. Check-ins it made offline before the revoke are still accepted for 24 hours and land in the [report](#checkin).

## Check-in report {#checkin}

`GET /v1/events/{id}/checkin`

```bash Request
curl https://api.budu.events/v1/events/0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60/checkin \
  -H "Authorization: Bearer $BUDU_KEY"
```

```json Response 200
{
  "counter": {"total": 212, "passed": 187},
  "invalid_scans": 3,
  "double_passes": [
    {
      "ticket_id": "0193e0c5-1f6b-7e02-a3c9-5b8d1e4f9f0a",
      "number": "1E4F9F0A",
      "holder": "Мария Соколова",
      "passes": [
        {
          "scan_id": "s-81f2",
          "entrance": "Вход 1",
          "scanned_at_ms": "1797096132000",
          "offline": true,
          "revoked_device": false,
          "manual": false
        },
        {
          "scan_id": "s-0c4a",
          "entrance": "Вход 2",
          "scanned_at_ms": "1797096190000",
          "offline": true,
          "revoked_device": false,
          "manual": false
        }
      ]
    }
  ],
  "revoked_device_passes": []
}
```

| Field | Description |
|---|---|
| `counter` | `total` counts the event's valid tickets, `passed` how many of them checked in. Door staff see the same numbers as «Прошло N / M» (N / M in) |
| `invalid_scans` | How many scans ended as "not valid": another event's or a cancelled ticket, an old QR, a fake |
| `double_passes` | Up to 100 tickets that checked in more than once. The first check-in in `passes` is the real one, the rest are doubles. This happens when two offline entrances let the same ticket in |
| `revoked_device_passes` | Up to 100 tickets whose real check-in came from a revoked phone. The guest is already in, but such a check-in is worth a look |

A check-in has `scan_id`, `entrance` (the device name), `scanned_at_ms`, `offline` (made offline), `manual` (door staff let the guest in by hand), `revoked_device` (from a revoked phone).
