Passdesk

passdesk

API Reference

Programmatic access for Scale-tier driving schools. v1 — read-mostly.

Download OpenAPI spec (YAML)

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 in pagination.total and the X-Total-Count header.
  • 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-Remaining headers on every response; 429 with Retry-After on overage.

Endpoints

Auth

GET
/me

Sanity-check the bearer key. Returns clientId, apiKeyId, readOnly.

Students

GET
/students

List students. Supports ?limit and ?offset.

GET
/students/{id}

Get a single student.

Schedules (lessons)

GET
/schedules

List lessons. Filter by ?from, ?to, ?status, ?studentId, ?instructorId.

GET
/schedules/{id}

Get a single lesson.

Products

GET
/products

List products. Filter ?sellable=true.

GET
/products/{id}

Get a single product.

Purchases

GET
/purchases

List purchases. Filter ?studentId, ?status, ?kind.

GET
/purchases/{id}

Get a single purchase.

Progress

GET
/progress

List progress entries. Filter ?studentId, ?from.

GET
/progress/{id}

Get a single progress entry.

Instructors

GET
/instructors

List instructors in the tenant.

GET
/instructors/{id}

Get a single instructor.

Employees

GET
/employees

List office employees (non-instructor staff).

GET
/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.