# pypretix-device A minimal Python API client for [pretix](https://pretix.eu) device authentication and ticket scanning. Designed for kiosk/scan-station devices that need to check volunteers or attendees in and out via QR/barcode scanning. ## Installation ```bash pip install pypretix-device ``` Editable install for development: ```bash pip install -e pypretix-device ``` ## Quick Start ```python from pypretix_device import auth, PretixDeviceClient from pypretix_device.models import CheckinResult # --- On first use: register device --------------------------------- # Go to Organizer Settings → Devices in the pretix web UI to get a # 24-character initialization token, then: device = auth.get_or_register_device( base_url="http://localhost", init_token="xxxxxxxxxxxxxxxxxxxxxxxx", # from pretix web UI device_name="example-scanner", ) # Config is saved to ~/.config/pypretix/config.json automatically # On subsequent runs it loads the stored token: device = auth.get_or_register_device( base_url="http://localhost", init_token=None, # no token needed anymore ) # --- Create the client --------------------------------------------- client = PretixDeviceClient( base_url="http://localhost", api_token=device.api_token, organizer_slug="FL", event_slug="PB26", checkin_list_id=1, ) # --- Check a ticket in (entry) ------------------------------------- result = client.checkin("qrCodeSecretFromBarcode") if result.status == "ok": print(f"Welcome: {result.position['attendee_name']}") else: print(f"Error: {result.reason}") # e.g. "invalid", "already_redeemed" # --- Check a ticket out (exit) ------------------------------------- result = client.checkout("qrCodeSecretFromBarcode") if result.status == "ok": print(f"Checked out: {result.position['attendee_name']}") # --- Look up ticket status (Infodesk) ------------------------------ result = client.search("qrCodeSecretFromBarcode") if result.position: print(f"Found: {result.position['attendee_name']}") print(f"Checkins: {len(result.position.get('checkins', []))}") for ci in result.position['checkins']: print(f" {ci['datetime'][:19]} - {ci['type']}") else: print("Token not found") # --- List events --------------------------------------------------- events = client.list_events() for e in events: print(f"{e.slug} — {e.name}") # --- List check-in lists for the current event --------------------- lists = client.list_checkin_lists() for cl in lists: print(f"id={cl.id}: {cl.name} ({cl.checkin_count}/{cl.position_count})") # --- Device info --------------------------------------------------- info = client.device_info() print(f"Device ID: {info.device_id}") print(f"Server: {info.server_version}") ``` ## API Reference ### `pypretix_device.auth` — Device Registration & Token Management #### `get_or_register_device(base_url, init_token=None, device_name="example-scanner")` Load stored device config, or register a new device if no config exists. | Parameter | Description | |-----------|-------------| | `base_url` | pretix instance URL, e.g. `"http://localhost"` or `"https://events.example.com"` | | `init_token` | 24-char initialization token from pretix web UI (Organizer Settings → Devices). Required only on first run. | | `device_name` | Human-readable name for the device. | **Returns:** `DeviceInfo` object with `api_token`, `device_id`, `organizer`, etc. **Raises:** `RegistrationFailedError` if no config exists and no `init_token` provided. #### `register_device(base_url, init_token, device_name="helferscanner")` Register a new device. Same parameters as above. The device must already exist in the pretix web UI (with an initialization token created). The `init_token` is consumed and cannot be reused. **Returns:** `DeviceInfo` with the new device API token. #### `revoke_device()` Permanently invalidate the device's API token and remove the config file. The device must be deleted from the pretix web UI as well. ### `PretixDeviceClient` — Ticket Scanning Operations Create with: ```python client = PretixDeviceClient( base_url: str, # pretix instance URL api_token: str, # Device API token (from registration) organizer_slug: str, # Organizer slug, e.g. "FL" event_slug: str, # Event slug, e.g. "PB26" checkin_list_id: int | None, # Check-in list ID to use (e.g. 1) ) ``` #### `checkin(barcode, lists=None) -> CheckinResult` Check a ticket in (entry). Sets `type="entry"` on the checkinrpc/redeem endpoint. Uses `force=true` to handle pending/unpaid orders gracefully. | Parameter | Description | |-----------|-------------| | `barcode` | The QR code secret string scanned from the ticket | | `lists` | Optional override for check-in list IDs. Falls back to client default. | **Returns:** `CheckinResult` - `status`: `"ok"` on success, `"error"` on failure, `"incomplete"` if questions need answering - `reason`: Error code string (e.g. `"invalid"`, `"already_redeemed"`, `"unpaid"`, `"blocked"`) - `position`: Order position data (attendee name, item info, checkin history) - `require_attention`: `True` if the item/order has the checkin_attention flag set - `checkin_texts`: Additional display strings for the user - `questions`: Required question list if `status == "incomplete"` #### `checkout(barcode, lists=None) -> CheckinResult` Check a ticket out (exit). Sets `type="exit"` on the checkinrpc/redeem endpoint. Same parameters and return type as `checkin()`. #### `search(barcode, lists=None) -> SearchResult` Read-only ticket lookup (Infodesk mode). Queries checkinrpc/search for the ticket secret. | Parameter | Description | |-----------|-------------| | `barcode` | QR code secret or attendee name to search | | `lists` | Optional check-in list IDs to search within | **Returns:** `SearchResult` - `position`: Full order position dict with `checkins` history (list of `{datetime, type, gate}`) - `require_attention`: `True` if the item/order has checkin_attention flag set #### `list_events() -> list[Event]` List all events for the configured organizer. **Returns:** `list[Event]` with fields: `slug`, `name`, `testmode`, `date_from`, `date_to`. #### `list_checkin_lists(event_slug=None) -> list[CheckinList]` List check-in lists for the current (or specified) event. **Returns:** `list[CheckinList]` with fields: `id`, `name`, `all_products`, `checkin_count`, `position_count`, `allow_entry_after_exit`. #### `select_event(event_slug) -> bool` Switch the client to a different event. Returns `True` on success. #### `device_info() -> DeviceInfo \| None` Fetch device information from the pretix server (device ID, server version, gate assignment). #### `device_update(software_version) -> DeviceInfo \| None` Notify the server about a software version update. #### Connection status properties | Property | Description | |----------|-------------| | `client.is_connected` | `True` if the last API request succeeded | | `client.last_error` | Error message string, or `None` | | `client.last_response_time` | Timestamp (float) of last successful response | ### Data Models See `pypretix_device.models`: | Class | Description | |-------|-------------| | `CheckinResult` | Result of checkin/checkout operations | | `SearchResult` | Result of search (read-only) operations | | `Event` | Event data (slug, name, dates) | | `CheckinList` | Check-in list data (id, name, counts) | | `DeviceInfo` | Device metadata (id, organizer, server version) | | `AttendeeInfo` | Personalized ticket attendee data | ## Configuration Config is stored in `~/.config/pypretix/config.json`: ```json { "base_url": "http://localhost", "api_token": "a1b2c3d4e5f6...", "device_id": 5, "organizer": "TEST", "device_name": "example-scanner", "software_version": "0.0.1" } ``` To use a custom config directory: ```python import pypretix_device.models pypretix_device.models.CONFIG_DIR = "/path/to/custom/config" ``` ## Environment Variables (for running the app) ```bash export PRETIX_BASE_URL="http://localhost" # pretix instance URL export PRETIX_ORGANIZER="TEST" # organizer slug export PRETIX_EVENT="PB26" # event slug export PRETIX_CHECKIN_LIST="1" # check-in list ID export PRETIX_DEVICE_INIT_TOKEN="xxx" # only needed on first run ``` ## Error Codes The `reason` field in `CheckinResult` contains one of these codes: | Code | Meaning | |------|---------| | `invalid` | Ticket barcode not known | | `already_redeemed` | Ticket already checked in | | `unpaid` | Order not paid for | | `blocked` | Ticket has been blocked | | `invalid_time` | Ticket outside valid time window | | `canceled` | Ticket has been canceled | | `ambiguous` | Multiple tickets match — can't resolve | | `revoked` | Ticket secret has been revoked | | `unapproved` | Order not yet approved by organizer | | `product` | Ticket product not allowed on this check-in list | | `rules` | Check-in prevented by organizer-defined rules | | `incomplete` | Required questions need answers (only with `questions_supported: True`) | | `error` | Internal server error | ## Lifecycle Example ### 1. Initial device setup (one-time) 1. Log into pretix web UI as organizer admin 2. Go to **Organizer Settings → Devices → New Device** 3. Name the device (e.g. "Example-Scanner") 4. Note the **Initialization Token** (24 characters) 5. Store it as env var: `PRETIX_DEVICE_INIT_TOKEN=xxxxx` 6. On first run, `get_or_register_device()` uses this token and saves the device API token to config ### 2. Normal operation (daily) On every subsequent run, config is loaded automatically — no token needed: ```python device = get_or_register_device(base_url, init_token=None) client = PretixDeviceClient(..., api_token=device.api_token) result = client.checkin(barcode) ``` ### 3. Ticket check-in flow ``` Scanner reads QR code → barcode string → client.checkin(barcode) → status="ok" → show OK + name → status="error" → show error code (e.g. "Bereits gescannt") ``` ### 4. Ticket check-out flow ``` Scanner reads QR code → barcode string → client.checkout(barcode) → status="ok" → show OK + name → status="error" → show error code ``` ### 5. Info desk lookup ``` Scanner reads QR code → barcode string → client.search(barcode) → position exists → show attendee name + checkin history → position None → show "Token nicht gefunden" ``` ### 6. Deprovisioning When the device is no longer needed: ```python from pypretix_device.auth import revoke_device revoke_device() # invalidates token + removes config ``` Also delete the device from the pretix web UI (Organizer Settings → Devices). ## License GNUGPLv3