For developers

PAT Control API

Read and write your organisation's clients, sites, appliances and test results, and read instruments and invoices.

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.

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.

Rate limits. 120 requests a minute per key. X-RateLimit-Remaining and X-RateLimit-Reset are on every response; a 429 carries Retry-After.

Test records are legal evidence: they can be recorded and voided (with a reason) but never edited or deleted.

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.

Base address: https://app.patcontrol.co.uk/api/v1 · Machine-readable description: openapi.json (OpenAPI 3.1; import it into Postman or a client generator).

Quick start

curl https://<your address>/api/v1/appliances?due_before=2026-12-31 \
  -H "Authorization: Bearer pat_…"

Endpoints

Organisation

get/meThe key's organisation and scopesany

Clients

get/clientsList clientsclients:read
ParameterTypeNotes
limitinteger
cursorstringnext_cursor from the previous page
updated_sincestring (date-time)
include_archivedboolean
post/clientsCreate a clientclients:write
get/clients/{id}Get a clientclients:read
ParameterTypeNotes
id (path)string (uuid)
patch/clients/{id}Update or archive a clientclients:write

Send only the fields to change. null clears an optional field.

ParameterTypeNotes
id (path)string (uuid)

Body: ClientUpdate

Sites

get/sitesList sitesclients:read
ParameterTypeNotes
limitinteger
cursorstringnext_cursor from the previous page
client_idstring (uuid)
updated_sincestring (date-time)
include_archivedboolean
post/sitesCreate a site for a clientclients:write
get/sites/{id}Get a siteclients:read
ParameterTypeNotes
id (path)string (uuid)
patch/sites/{id}Update or archive a siteclients:write

Send only the fields to change. null clears an optional field.

ParameterTypeNotes
id (path)string (uuid)

Body: SiteUpdate

Appliances

get/appliancesList appliances (the asset register)appliances:read
ParameterTypeNotes
limitinteger
cursorstringnext_cursor from the previous page
client_idstring (uuid)
site_idstring (uuid)
asset_idstringExact match, any case
status"in_service" | "failed" | "repaired" | "removed"
due_beforestring (date)Appliances due for retest on or before this date
updated_sincestring (date-time)
post/appliancesAdd an appliance to a siteappliances:write
get/appliances/{id}Get an applianceappliances:read
ParameterTypeNotes
id (path)string (uuid)
patch/appliances/{id}Update an applianceappliances:write

Send only the fields to change. null clears an optional field.

ParameterTypeNotes
id (path)string (uuid)

Body: ApplianceUpdate

Tests

get/testsList test results with their readingstests:read
ParameterTypeNotes
limitinteger
cursorstringnext_cursor from the previous page
appliance_idstring (uuid)
site_idstring (uuid)
result"pass" | "fail"
tested_fromstring (date)
tested_tostring (date)
recorded_sincestring (date-time)Tests recorded here at or after this time (for syncing)
post/testsRecord a testtests:write

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.

Body: TestCreate

get/tests/{id}Get a test with its readingstests:read
ParameterTypeNotes
id (path)string (uuid)
post/tests/{id}/voidVoid a testtests:write

The test stays on record, marked void with the reason, and the appliance goes back to its latest remaining test.

ParameterTypeNotes
id (path)string (uuid)

Body: TestVoid

Instruments

get/instrumentsList test instrumentsinstruments:read
ParameterTypeNotes
limitinteger
cursorstringnext_cursor from the previous page
get/instruments/{id}Get an instrumentinstruments:read
ParameterTypeNotes
id (path)string (uuid)

Invoices

get/invoicesList invoices with their linesinvoices:read
ParameterTypeNotes
limitinteger
cursorstringnext_cursor from the previous page
client_idstring (uuid)
status"draft" | "sent" | "paid" | "void"
updated_sincestring (date-time)
get/invoices/{id}Get an invoiceinvoices:read
ParameterTypeNotes
id (path)string (uuid)

Verifying webhooks

Each delivery carries X-Webhook-Event, X-Webhook-Id (the event id) and X-Webhook-Signature. In Node.js:

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as received (a string), before JSON parsing.
export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!parts.t || !parts.v1 || age > 300) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Objects

Error

errorobject

Client

object"client"
idstring (uuid)
namestringClient name, unique among your active clients.
contact_namestring | null
emailstring | null (email)
phonestring | null
address_linesarrayUp to six lines.
townstring | null
countystring | null
postcodestring | null
notesstring | null
archivedboolean
created_atstring (date-time)
updated_atstring (date-time)

Site

object"site"
idstring (uuid)
client_idstring (uuid)
namestringSite name, unique within the client.
address_linesarray
townstring | null
postcodestring | null
environment"construction" | "industrial" | "public" | "school" | "hotel" | "office" | "hire"Drives the default retest interval (IET Code of Practice).
notesstring | null
archivedboolean
created_atstring (date-time)
updated_atstring (date-time)

Appliance

object"appliance"
idstring (uuid)
client_idstring (uuid)
site_idstring (uuid)
locationstring | nullRoom or area on the site; created if new.
asset_idstringThe label on the appliance. Unique per site.
descriptionstring
equipment_class"class_i" | "class_ii" | "class_iii" | "lead"
category"portable" | "movable" | "hand_held" | "stationary" | "fixed" | "built_in" | "it" | "extension_lead"
makestring | null
modelstring | null
serialstring | null
fuse_rating_ainteger | nullFuse rating in amps (1–32).
status"in_service" | "failed" | "repaired" | "removed"
retest_monthsinteger | nullOverrides the interval from site and organisation rules (1–120).
notesstring | null
last_tested_atstring | null (date-time)
last_result"pass" | "fail"
next_due_onstring | null (date)Last test date plus the retest interval.
created_atstring (date-time)
updated_atstring (date-time)

Measurement

kind *"visual" | "earth_continuity" | "insulation" | "protective_conductor_current" | "touch_current" | "substitute_leakage" | "polarity" | "functional" | "rcd_trip_time" | "load"
labelstringDefaults to the standard name for the kind.
unitstring | nullΩ, MΩ, mA or ms; set from the kind.
limitnumber | null
limit_qualifier"<" | ">"
resultnumber | null
result_qualifier"<" | ">"For off-scale readings, e.g. > 299 MΩ.
result_textstring | null
test_voltage_vinteger | null
status"pass" | "fail" | "na"Numeric readings are re-judged against their limit; checks use the status you send.

Test

object"test"
idstring (uuid)
appliance_idstring (uuid)
tested_atstring (date-time)
engineer_namestring
instrument_idstring | null (uuid)
result"pass" | "fail"
kind"visual" | "combined"
retest_monthsinteger
next_due_onstring (date)
notesstring | null
source"manual" | "kewpat" | "csv" | "api"
external_idstring | nullThe external_id sent with a test recorded through the API.
test_codestring | null
voidedobject | null
appliance_snapshotobjectThe appliance and location as tested (what the certificate shows).
measurementsMeasurement[]
created_atstring (date-time)When the test was recorded here.

Instrument

object"instrument"
idstring (uuid)
makestring
modelstring
serialstring
calibrated_onstring | null (date)
calibration_due_onstring | null (date)
certificate_numberstring | null
archivedboolean
created_atstring (date-time)
updated_atstring (date-time)

Invoice

object"invoice"
idstring (uuid)
client_idstring (uuid)
numberstring | nullSet when the invoice is issued.
status"draft" | "sent" | "paid" | "void"
issue_datestring | null (date)
due_datestring | null (date)
referencestring | null
currency"GBP"
subtotal_penceinteger
vat_penceinteger
total_penceinteger
paid_atstring | null (date-time)
externalobject | null
linesarray
created_atstring (date-time)
updated_atstring (date-time)

Organisation

object"organisation"
idstring (uuid)
namestring
slugstring
api_keyobject

WebhookEvent

idstring (uuid)The event id; the same for every endpoint. Use it to ignore repeats.
type"client.created" | "appliance.created" | "appliance.updated" | "test.recorded" | "test.voided" | "invoice.issued" | "invoice.paid" | "ping"
created_atstring (date-time)
dataobjectA summary of the record: object, id and key fields. Fetch the full record from the API if you need more.

ClientCreate

name *stringClient name, unique among your active clients.
contact_namestring | null
emailstring | null (email)
phonestring | null
address_linesarrayUp to six lines.
townstring | null
countystring | null
postcodestring | null
notesstring | null

SiteCreate

client_id *string (uuid)
name *stringSite name, unique within the client.
address_linesarray
townstring | null
postcodestring | null
environment"construction" | "industrial" | "public" | "school" | "hotel" | "office" | "hire"Drives the default retest interval (IET Code of Practice).
notesstring | null

ApplianceCreate

site_id *string (uuid)
locationstring | nullRoom or area on the site; created if new.
asset_id *stringThe label on the appliance. Unique per site.
description *string
equipment_class *"class_i" | "class_ii" | "class_iii" | "lead"
category"portable" | "movable" | "hand_held" | "stationary" | "fixed" | "built_in" | "it" | "extension_lead"
makestring | null
modelstring | null
serialstring | null
fuse_rating_ainteger | nullFuse rating in amps (1–32).
status"in_service" | "failed" | "repaired" | "removed"
retest_monthsinteger | nullOverrides the interval from site and organisation rules (1–120).
notesstring | null

TestCreate

appliance_id *string (uuid)
tested_at *string (date-time)With a time zone offset, e.g. 2026-10-14T09:30:00Z.
engineer_name *string
instrument_idstring | null (uuid)
kind"visual" | "combined"
retest_monthsinteger | nullLeave out to use the appliance, site and organisation rules.
notesstring | null
test_codestring | null
external_idstringYour system's id for this test. Sending the same id again is refused with 409, so retries are safe.
result"pass" | "fail"Your tester's overall verdict; kept when it is stricter than ours.
measurements *Measurement[]1–30 readings and checks. Numeric kinds need limit and result (or a status); checks need a status.

TestVoid

reason *stringAt least 3 characters.

* required when creating. Questions? Help centre.