passdesk
API Reference
Programmatic access for Scale-tier driving schools. v1 — read-mostly.
Getting started
Mint a key in Passdesk under Your school → API Keys. The
plaintext is shown once at creation — copy it then; we don't store it. Pass the token
as a Bearer header on every request:
curl https://app.passdesk.co.uk/api/v1/students \ -H "Authorization: Bearer pdsk_live_<prefix>_<secret>"
Conventions
- Base URL:
https://app.passdesk.co.uk/api/v1 - Auth:
Authorization: Bearer pdsk_live_… - Pagination:
?limit(max 200, default 50) +?offset. Total count is inpagination.totaland theX-Total-Countheader. - Response envelope:
lists are
{ data: [...], pagination: { ... } }; detail responses are{ data: { ... } }. - Errors:
{ error: string, code?: string }with HTTP status.401= missing/invalid key,402= not on Scale or subscription inactive,403= read-only key on a write,404= not found in your tenant. - Rate limits:
per-key, 600 reads/min and 120 writes/min. Standard
RateLimit-Limit/RateLimit-Remainingheaders on every response;429withRetry-Afteron overage.
Endpoints
Auth
/me Sanity-check the bearer key. Returns clientId, apiKeyId, readOnly.
Students
/students List students. Supports ?limit and ?offset.
/students/{id} Get a single student.
Schedules (lessons)
/schedules List lessons. Filter by ?from, ?to, ?status, ?studentId, ?instructorId.
/schedules/{id} Get a single lesson.
Products
/products List products. Filter ?sellable=true.
/products/{id} Get a single product.
Purchases
/purchases List purchases. Filter ?studentId, ?status, ?kind.
/purchases/{id} Get a single purchase.
Progress
/progress List progress entries. Filter ?studentId, ?from.
/progress/{id} Get a single progress entry.
Instructors
/instructors List instructors in the tenant.
/instructors/{id} Get a single instructor.
Employees
/employees List office employees (non-instructor staff).
/employees/{id} Get a single employee.
Read-only keys
Tick the read-only box at creation time for sync / reporting integrations that don't
need to mutate. A read-only key is a hard 403 on any
POST, PUT,
PATCH, or DELETE across the entire
surface, regardless of which endpoint is called.
Webhooks
Subscribe an HTTPS endpoint to domain events and receive HMAC-signed JSON payloads
when things happen. No polling required. Manage endpoints in the workspace under
Webhooks.
Envelope
Every dispatch posts JSON of this shape, with
Idempotency-Key,
Passdesk-Event, and
Passdesk-Signature headers.
{
"id": "evt_01HFXZ...", // ULID-ish; doubles as Idempotency-Key
"type": "lesson.completed",
"createdAt": "2026-04-29T14:03:11Z",
"tenantId": 42,
"apiVersion": "v1",
"data": { ... } // shape depends on type
} Event types (MVP)
student.created New student row
schedule.cancelled Booking cancelled (carries fee status)
lesson.completed Instructor closed the lesson with a rubric
rubric.updated Per-skill rating change
purchase.created Cash sale or Stripe checkout completion
Verifying signatures
The Passdesk-Signature header has the form
t=<unix>,v1=<hex>. The signed string is
`${t}.${rawBody}`. Compare in constant time. Reject
anything where |now - t| exceeds 5 minutes.
// Node.js (no dependencies)
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((p) => p.split('=')),
);
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > 5 * 60) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(parts.v1, 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
} Retry behaviour
Failed deliveries retry on an exponential schedule: 30s, 2m, 10m, 1h, 6h, 24h. After
seven attempts the row is marked failed; you can re-send it from the delivery log. We
deliver at-least-once — dedupe on the envelope's id (it's
stable across retries). Ordering is not guaranteed; reconcile on
createdAt.
Questions? Drop a note via the in-app feedback widget — every report lands on the founder's desk.