{"openapi":"3.1.0","info":{"title":"PAT Control API","version":"1.0.0","description":"Read and write your organisation's clients, sites, appliances and test results, and read instruments and invoices.\n\n**Authentication.** Create a key in Settings → API & webhooks and send it as `Authorization: Bearer pat_…`. Each key belongs to one organisation and has scopes; a write scope includes read. Changes made with a key are recorded in the audit log against the person who created it.\n\n**Pagination.** Lists return `data`, `has_more` and `next_cursor`, oldest first. Pass `cursor` to get the next page. To sync changes, poll with `updated_since` (or `recorded_since` for tests), or subscribe to webhooks.\n\n**Rate limits.** 120 requests a minute per key. `X-RateLimit-Remaining` and `X-RateLimit-Reset` are on every response; a 429 carries `Retry-After`.\n\n**Test records** are legal evidence: they can be recorded and voided (with a reason) but never edited or deleted.\n\n**Webhooks.** Endpoints receive a POST with a JSON event for: client.created, appliance.created, appliance.updated, test.recorded, test.voided, invoice.issued, invoice.paid. Verify the `X-Webhook-Signature` header: `t=<unix time>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\" using your endpoint secret>`; reject timestamps more than 5 minutes old. Reply with any 2xx within 10 seconds; failures are retried for about 2 days."},"servers":[{"url":"https://app.patcontrol.co.uk/api/v1"}],"tags":[{"name":"Organisation"},{"name":"Clients"},{"name":"Sites"},{"name":"Appliances"},{"name":"Tests"},{"name":"Instruments"},{"name":"Invoices"}],"paths":{"/me":{"get":{"tags":["Organisation"],"summary":"The key's organisation and scopes","security":[{"apiKey":[]}],"x-scope":"any","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organisation"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/clients":{"get":{"tags":["Clients"],"summary":"List clients","security":[{"apiKey":[]}],"x-scope":"clients:read","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"next_cursor from the previous page"},{"name":"updated_since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"include_archived","in":"query","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Client"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pass as cursor to get the next page."}},"required":["data","has_more","next_cursor"]}}}},"400":{"description":"Bad request (e.g. malformed cursor or JSON)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Clients"],"summary":"Create a client","security":[{"apiKey":[]}],"x-scope":"clients:write","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The organisation is read-only until it has an active subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Body isn't JSON","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/clients/{id}":{"get":{"tags":["Clients"],"summary":"Get a client","security":[{"apiKey":[]}],"x-scope":"clients:read","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Clients"],"summary":"Update or archive a client","description":"Send only the fields to change. null clears an optional field.","security":[{"apiKey":[]}],"x-scope":"clients:write","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The organisation is read-only until it has an active subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Body isn't JSON","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sites":{"get":{"tags":["Sites"],"summary":"List sites","security":[{"apiKey":[]}],"x-scope":"clients:read","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"next_cursor from the previous page"},{"name":"client_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"updated_since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"include_archived","in":"query","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Site"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pass as cursor to get the next page."}},"required":["data","has_more","next_cursor"]}}}},"400":{"description":"Bad request (e.g. malformed cursor or JSON)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Sites"],"summary":"Create a site for a client","security":[{"apiKey":[]}],"x-scope":"clients:write","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Site"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The organisation is read-only until it has an active subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Body isn't JSON","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sites/{id}":{"get":{"tags":["Sites"],"summary":"Get a site","security":[{"apiKey":[]}],"x-scope":"clients:read","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Site"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Sites"],"summary":"Update or archive a site","description":"Send only the fields to change. null clears an optional field.","security":[{"apiKey":[]}],"x-scope":"clients:write","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Site"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The organisation is read-only until it has an active subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Body isn't JSON","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/appliances":{"get":{"tags":["Appliances"],"summary":"List appliances (the asset register)","security":[{"apiKey":[]}],"x-scope":"appliances:read","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"next_cursor from the previous page"},{"name":"client_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"site_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"asset_id","in":"query","schema":{"type":"string"},"description":"Exact match, any case"},{"name":"status","in":"query","schema":{"type":"string","enum":["in_service","failed","repaired","removed"]}},{"name":"due_before","in":"query","schema":{"type":"string","format":"date"},"description":"Appliances due for retest on or before this date"},{"name":"updated_since","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Appliance"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pass as cursor to get the next page."}},"required":["data","has_more","next_cursor"]}}}},"400":{"description":"Bad request (e.g. malformed cursor or JSON)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Appliances"],"summary":"Add an appliance to a site","security":[{"apiKey":[]}],"x-scope":"appliances:write","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplianceCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Appliance"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The organisation is read-only until it has an active subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Body isn't JSON","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/appliances/{id}":{"get":{"tags":["Appliances"],"summary":"Get an appliance","security":[{"apiKey":[]}],"x-scope":"appliances:read","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Appliance"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Appliances"],"summary":"Update an appliance","description":"Send only the fields to change. null clears an optional field.","security":[{"apiKey":[]}],"x-scope":"appliances:write","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplianceUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Appliance"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The organisation is read-only until it has an active subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Body isn't JSON","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tests":{"get":{"tags":["Tests"],"summary":"List test results with their readings","security":[{"apiKey":[]}],"x-scope":"tests:read","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"next_cursor from the previous page"},{"name":"appliance_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"site_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"result","in":"query","schema":{"type":"string","enum":["pass","fail"]}},{"name":"tested_from","in":"query","schema":{"type":"string","format":"date"}},{"name":"tested_to","in":"query","schema":{"type":"string","format":"date"}},{"name":"recorded_since","in":"query","schema":{"type":"string","format":"date-time"},"description":"Tests recorded here at or after this time (for syncing)"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Test"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pass as cursor to get the next page."}},"required":["data","has_more","next_cursor"]}}}},"400":{"description":"Bad request (e.g. malformed cursor or JSON)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Tests"],"summary":"Record a test","description":"Moves the appliance's last result and next due date on when this is its latest test. The pass/fail verdict is worked out from the readings.","security":[{"apiKey":[]}],"x-scope":"tests:write","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Test"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The organisation is read-only until it has an active subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Body isn't JSON","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tests/{id}":{"get":{"tags":["Tests"],"summary":"Get a test with its readings","security":[{"apiKey":[]}],"x-scope":"tests:read","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Test"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tests/{id}/void":{"post":{"tags":["Tests"],"summary":"Void a test","description":"The test stays on record, marked void with the reason, and the appliance goes back to its latest remaining test.","security":[{"apiKey":[]}],"x-scope":"tests:write","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestVoid"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Test"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The organisation is read-only until it has an active subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Body isn't JSON","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/instruments":{"get":{"tags":["Instruments"],"summary":"List test instruments","security":[{"apiKey":[]}],"x-scope":"instruments:read","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"next_cursor from the previous page"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Instrument"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pass as cursor to get the next page."}},"required":["data","has_more","next_cursor"]}}}},"400":{"description":"Bad request (e.g. malformed cursor or JSON)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/instruments/{id}":{"get":{"tags":["Instruments"],"summary":"Get an instrument","security":[{"apiKey":[]}],"x-scope":"instruments:read","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Instrument"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/invoices":{"get":{"tags":["Invoices"],"summary":"List invoices with their lines","security":[{"apiKey":[]}],"x-scope":"invoices:read","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"next_cursor from the previous page"},{"name":"client_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"status","in":"query","schema":{"type":"string","enum":["draft","sent","paid","void"]}},{"name":"updated_since","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pass as cursor to get the next page."}},"required":["data","has_more","next_cursor"]}}}},"400":{"description":"Bad request (e.g. malformed cursor or JSON)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation failed; see error.fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/invoices/{id}":{"get":{"tags":["Invoices"],"summary":"Get an invoice","security":[{"apiKey":[]}],"x-scope":"invoices:read","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"401":{"description":"Missing, invalid, expired or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{"client.created":{"post":{"summary":"client.created","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event"}}}},"appliance.created":{"post":{"summary":"appliance.created","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event"}}}},"appliance.updated":{"post":{"summary":"appliance.updated","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event"}}}},"test.recorded":{"post":{"summary":"test.recorded","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event"}}}},"test.voided":{"post":{"summary":"test.voided","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event"}}}},"invoice.issued":{"post":{"summary":"invoice.issued","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event"}}}},"invoice.paid":{"post":{"summary":"invoice.paid","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"An API key: pat_…"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"Machine-readable, e.g. not_found, invalid_request, forbidden."},"message":{"type":"string"},"fields":{"type":"object","additionalProperties":{"type":"string"}}},"required":["type","message"]}}},"Client":{"type":"object","properties":{"object":{"type":"string","enum":["client"]},"id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Client name, unique among your active clients."},"contact_name":{"type":["string","null"]},"email":{"type":["string","null"],"format":"email"},"phone":{"type":["string","null"]},"address_lines":{"type":"array","items":{"type":"string"},"description":"Up to six lines."},"town":{"type":["string","null"]},"county":{"type":["string","null"]},"postcode":{"type":["string","null"]},"notes":{"type":["string","null"]},"archived":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ClientCreate":{"type":"object","properties":{"name":{"type":"string","description":"Client name, unique among your active clients."},"contact_name":{"type":["string","null"]},"email":{"type":["string","null"],"format":"email"},"phone":{"type":["string","null"]},"address_lines":{"type":"array","items":{"type":"string"},"description":"Up to six lines."},"town":{"type":["string","null"]},"county":{"type":["string","null"]},"postcode":{"type":["string","null"]},"notes":{"type":["string","null"]}},"required":["name"]},"ClientUpdate":{"type":"object","properties":{"name":{"type":"string","description":"Client name, unique among your active clients."},"contact_name":{"type":["string","null"]},"email":{"type":["string","null"],"format":"email"},"phone":{"type":["string","null"]},"address_lines":{"type":"array","items":{"type":"string"},"description":"Up to six lines."},"town":{"type":["string","null"]},"county":{"type":["string","null"]},"postcode":{"type":["string","null"]},"notes":{"type":["string","null"]},"archived":{"type":"boolean"}}},"Site":{"type":"object","properties":{"object":{"type":"string","enum":["site"]},"id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Site name, unique within the client."},"address_lines":{"type":"array","items":{"type":"string"}},"town":{"type":["string","null"]},"postcode":{"type":["string","null"]},"environment":{"type":["string","null"],"description":"Drives the default retest interval (IET Code of Practice).","enum":["construction","industrial","public","school","hotel","office","hire"]},"notes":{"type":["string","null"]},"archived":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"SiteCreate":{"type":"object","properties":{"client_id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Site name, unique within the client."},"address_lines":{"type":"array","items":{"type":"string"}},"town":{"type":["string","null"]},"postcode":{"type":["string","null"]},"environment":{"type":["string","null"],"description":"Drives the default retest interval (IET Code of Practice).","enum":["construction","industrial","public","school","hotel","office","hire"]},"notes":{"type":["string","null"]}},"required":["client_id","name"]},"SiteUpdate":{"type":"object","properties":{"name":{"type":"string","description":"Site name, unique within the client."},"address_lines":{"type":"array","items":{"type":"string"}},"town":{"type":["string","null"]},"postcode":{"type":["string","null"]},"environment":{"type":["string","null"],"description":"Drives the default retest interval (IET Code of Practice).","enum":["construction","industrial","public","school","hotel","office","hire"]},"notes":{"type":["string","null"]},"archived":{"type":"boolean"}}},"Appliance":{"type":"object","properties":{"object":{"type":"string","enum":["appliance"]},"id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid"},"site_id":{"type":"string","format":"uuid"},"location":{"type":["string","null"],"description":"Room or area on the site; created if new."},"asset_id":{"type":"string","description":"The label on the appliance. Unique per site."},"description":{"type":"string"},"equipment_class":{"type":"string","enum":["class_i","class_ii","class_iii","lead"]},"category":{"type":["string","null"],"enum":["portable","movable","hand_held","stationary","fixed","built_in","it","extension_lead"]},"make":{"type":["string","null"]},"model":{"type":["string","null"]},"serial":{"type":["string","null"]},"fuse_rating_a":{"type":["integer","null"],"description":"Fuse rating in amps (1–32)."},"status":{"type":"string","enum":["in_service","failed","repaired","removed"]},"retest_months":{"type":["integer","null"],"description":"Overrides the interval from site and organisation rules (1–120)."},"notes":{"type":["string","null"]},"last_tested_at":{"type":["string","null"],"format":"date-time"},"last_result":{"type":["string","null"],"enum":["pass","fail"]},"next_due_on":{"type":["string","null"],"description":"Last test date plus the retest interval.","format":"date"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ApplianceCreate":{"type":"object","properties":{"site_id":{"type":"string","format":"uuid"},"location":{"type":["string","null"],"description":"Room or area on the site; created if new."},"asset_id":{"type":"string","description":"The label on the appliance. Unique per site."},"description":{"type":"string"},"equipment_class":{"type":"string","enum":["class_i","class_ii","class_iii","lead"]},"category":{"type":["string","null"],"enum":["portable","movable","hand_held","stationary","fixed","built_in","it","extension_lead"]},"make":{"type":["string","null"]},"model":{"type":["string","null"]},"serial":{"type":["string","null"]},"fuse_rating_a":{"type":["integer","null"],"description":"Fuse rating in amps (1–32)."},"status":{"type":"string","enum":["in_service","failed","repaired","removed"]},"retest_months":{"type":["integer","null"],"description":"Overrides the interval from site and organisation rules (1–120)."},"notes":{"type":["string","null"]}},"required":["site_id","asset_id","description","equipment_class"]},"ApplianceUpdate":{"type":"object","properties":{"site_id":{"type":"string","description":"Move the appliance to another site.","format":"uuid"},"location":{"type":["string","null"],"description":"Room or area on the site; created if new."},"asset_id":{"type":"string","description":"The label on the appliance. Unique per site."},"description":{"type":"string"},"equipment_class":{"type":"string","enum":["class_i","class_ii","class_iii","lead"]},"category":{"type":["string","null"],"enum":["portable","movable","hand_held","stationary","fixed","built_in","it","extension_lead"]},"make":{"type":["string","null"]},"model":{"type":["string","null"]},"serial":{"type":["string","null"]},"fuse_rating_a":{"type":["integer","null"],"description":"Fuse rating in amps (1–32)."},"status":{"type":"string","enum":["in_service","failed","repaired","removed"]},"retest_months":{"type":["integer","null"],"description":"Overrides the interval from site and organisation rules (1–120)."},"notes":{"type":["string","null"]}}},"Measurement":{"type":"object","properties":{"kind":{"type":"string","enum":["visual","earth_continuity","insulation","protective_conductor_current","touch_current","substitute_leakage","polarity","functional","rcd_trip_time","load"]},"label":{"type":"string","description":"Defaults to the standard name for the kind."},"unit":{"type":["string","null"],"description":"Ω, MΩ, mA or ms; set from the kind."},"limit":{"type":["number","null"]},"limit_qualifier":{"type":["string","null"],"enum":["<",">"]},"result":{"type":["number","null"]},"result_qualifier":{"type":["string","null"],"description":"For off-scale readings, e.g. > 299 MΩ.","enum":["<",">"]},"result_text":{"type":["string","null"]},"test_voltage_v":{"type":["integer","null"]},"status":{"type":"string","description":"Numeric readings are re-judged against their limit; checks use the status you send.","enum":["pass","fail","na"]}},"required":["kind"]},"Test":{"type":"object","properties":{"object":{"type":"string","enum":["test"]},"id":{"type":"string","format":"uuid"},"appliance_id":{"type":"string","format":"uuid"},"tested_at":{"type":"string","format":"date-time"},"engineer_name":{"type":"string"},"instrument_id":{"type":["string","null"],"format":"uuid"},"result":{"type":"string","enum":["pass","fail"]},"kind":{"type":"string","enum":["visual","combined"]},"retest_months":{"type":"integer"},"next_due_on":{"type":"string","format":"date"},"notes":{"type":["string","null"]},"source":{"type":"string","enum":["manual","kewpat","csv","api"]},"external_id":{"type":["string","null"],"description":"The external_id sent with a test recorded through the API."},"test_code":{"type":["string","null"]},"voided":{"type":["object","null"],"properties":{"reason":{"type":"string"},"voided_at":{"type":"string","format":"date-time"}}},"appliance_snapshot":{"type":"object","description":"The appliance and location as tested (what the certificate shows)."},"measurements":{"type":"array","items":{"$ref":"#/components/schemas/Measurement"}},"created_at":{"type":"string","description":"When the test was recorded here.","format":"date-time"}}},"TestCreate":{"type":"object","properties":{"appliance_id":{"type":"string","format":"uuid"},"tested_at":{"type":"string","description":"With a time zone offset, e.g. 2026-10-14T09:30:00Z.","format":"date-time"},"engineer_name":{"type":"string"},"instrument_id":{"type":["string","null"],"format":"uuid"},"kind":{"type":"string","enum":["visual","combined"]},"retest_months":{"type":["integer","null"],"description":"Leave out to use the appliance, site and organisation rules."},"notes":{"type":["string","null"]},"test_code":{"type":["string","null"]},"external_id":{"type":"string","description":"Your system's id for this test. Sending the same id again is refused with 409, so retries are safe."},"result":{"type":"string","description":"Your tester's overall verdict; kept when it is stricter than ours.","enum":["pass","fail"]},"measurements":{"type":"array","items":{"$ref":"#/components/schemas/Measurement"},"description":"1–30 readings and checks. Numeric kinds need limit and result (or a status); checks need a status."}},"required":["appliance_id","tested_at","engineer_name","measurements"]},"TestVoid":{"type":"object","properties":{"reason":{"type":"string","description":"At least 3 characters."}},"required":["reason"]},"Instrument":{"type":"object","properties":{"object":{"type":"string","enum":["instrument"]},"id":{"type":"string","format":"uuid"},"make":{"type":"string"},"model":{"type":"string"},"serial":{"type":"string"},"calibrated_on":{"type":["string","null"],"format":"date"},"calibration_due_on":{"type":["string","null"],"format":"date"},"certificate_number":{"type":["string","null"]},"archived":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Invoice":{"type":"object","properties":{"object":{"type":"string","enum":["invoice"]},"id":{"type":"string","format":"uuid"},"client_id":{"type":"string","format":"uuid"},"number":{"type":["string","null"],"description":"Set when the invoice is issued."},"status":{"type":"string","enum":["draft","sent","paid","void"]},"issue_date":{"type":["string","null"],"format":"date"},"due_date":{"type":["string","null"],"format":"date"},"reference":{"type":["string","null"]},"currency":{"type":"string","enum":["GBP"]},"subtotal_pence":{"type":"integer"},"vat_pence":{"type":"integer"},"total_pence":{"type":"integer"},"paid_at":{"type":["string","null"],"format":"date-time"},"external":{"type":["object","null"],"properties":{"provider":{"type":"string","enum":["xero","quickbooks","freeagent"]},"id":{"type":["string","null"]},"synced_at":{"type":["string","null"],"format":"date-time"}}},"lines":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string"},"quantity":{"type":"number"},"unit_price_pence":{"type":"integer"},"vat_rate_bp":{"type":"integer","description":"Basis points: 2000 = 20%."},"net_pence":{"type":"integer"},"vat_pence":{"type":"integer"},"site_id":{"type":["string","null"],"format":"uuid"}}}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Organisation":{"type":"object","properties":{"object":{"type":"string","enum":["organisation"]},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"api_key":{"type":"object","properties":{"name":{"type":"string"},"prefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string","enum":["clients:read","clients:write","appliances:read","appliances:write","tests:read","tests:write","instruments:read","invoices:read"]}}}}}},"WebhookEvent":{"type":"object","properties":{"id":{"type":"string","description":"The event id; the same for every endpoint. Use it to ignore repeats.","format":"uuid"},"type":{"type":"string","enum":["client.created","appliance.created","appliance.updated","test.recorded","test.voided","invoice.issued","invoice.paid","ping"]},"created_at":{"type":"string","format":"date-time"},"data":{"type":"object","description":"A summary of the record: object, id and key fields. Fetch the full record from the API if you need more."}}}}}}