REST API
Authentication, resources, filtering, pagination, errors and limits.
Overview
The Doctilla API lets other systems read and update an organisation's quality records. It's a JSON REST API, bound by exactly the same permissions as the app.
| Base URL | https://mihqijyqxrggmcesqczp.supabase.co/functions/v1/api/v1 |
|---|---|
| OpenAPI 3.1 | https://mihqijyqxrggmcesqczp.supabase.co/functions/v1/api/v1/openapi.json (no key needed) |
| AI assistants | Use the connector rather than the REST API |
Settings → API keys in the app shows the current URLs. Dates are YYYY-MM-DD, timestamps ISO 8601, and request bodies must be application/json, up to 256 KB.
Authentication
Send an organisation API key on every request:
Authorization: Bearer dq_live_…Owners and admins create keys in Settings → API keys. A key is shown once; Doctilla stores only a hash of it and can't show it again.
A key acts as the person who created it
- Every request runs as the key's creator, in the key's organisation only, under the same database permissions as that person in the app. A key can never see or do more than its creator could.
- If the creator leaves the organisation or stops being an owner or admin, the key stops working. So does a revoked or expired key, and a key whose organisation's licence is suspended.
- Everyone using a key acts as that one person, so create keys from an account whose permissions you're happy for the integration to have.
- Module roles apply: a creator with no Product assurance role gets empty lists and 404s for product records.
Scopes
| Scope | Allows |
|---|---|
read | GET only |
read_write | GET, POST, PATCH and DELETE, subject to the creator's permissions |
Keys are server-side secrets. Never put a key in front-end code or a URL; keep it on a server or in a connector's settings, prefer read-only keys, and revoke a key the moment it may have leaked.
What a person decides
Doctilla's rule is that a person decides and Doctilla proposes. These fields can't be written through the API at all:
| Resource | Not writable |
|---|---|
| technical-reviews | conclusion, reviewer, status complete; a concluded review is final |
| management-reviews | approval and status minutes_approved; approved minutes are final |
| controlled-documents | approver, approval date, status approved |
| effectiveness-checks | result, assessor and observed value |
| standard-mappings | status and confirmation; mappings are created as proposed |
| claims | status supported |
| audits | lock fields; a locked audit is read-only |
| evidence | everything (read-only) |
Changing a finalised record returns 409 record_locked; writing a value only a person may set returns 422. A proposed CAR can only be set to open. Ids, organisation, timestamps, provenance fields and calculated fields such as a risk's score are never writable.
Resources
GET /v1 lists them; GET /v1/openapi.json gives every field, type and allowed value.
| Resource | Methods | Notes |
|---|---|---|
processes | GET POST PATCH DELETE | parent_id builds the tree |
audits | GET POST PATCH DELETE | locked audits are read-only |
audit-findings | GET POST PATCH DELETE | |
cars | GET POST PATCH DELETE | POST assigns car_number and creates the nine 8D steps; proposed CARs move only to open |
car-steps | GET PATCH | only content (merged key by key) and status; completing records who and when |
actions | GET POST PATCH DELETE | |
evidence | GET | metadata only |
nonconformities | GET POST PATCH DELETE | ncr_number assigned when omitted |
suppliers | GET POST PATCH DELETE | |
supplier-evaluations | GET POST PATCH DELETE | counts, never percentages |
risks | GET POST PATCH DELETE | reference assigned when omitted; score calculated |
quality-objectives | GET POST PATCH DELETE | |
kpis, kpi-measurements | GET POST PATCH DELETE | |
effectiveness-checks | GET POST PATCH DELETE | result recorded by a person |
management-reviews | GET POST PATCH DELETE | minutes approved by a person |
personnel, competencies, training-records | GET POST PATCH DELETE | personnel are not the same as Doctilla users |
controlled-documents | GET POST PATCH DELETE | revisions approved by a person |
standards, standard-clauses | GET POST PATCH DELETE | import only text you hold a licence for |
standard-mappings | GET POST PATCH DELETE | created as proposed only |
products, product-configurations, components, claims, publications, product-changes, technical-reviews | GET POST PATCH DELETE | Product assurance: needs a Product assurance role |
DELETE removes the record with everything that depends on it (links, conversations, history) and logs the deletion. Evidence files attached to a deleted audit, finding or CAR are removed from storage.
Other endpoints
GET /v1/me | The organisation, acting user, their role and the key's scope |
GET /v1/search?q=…&types=car,nonconformity&limit=10 | Text search across record types; limit is per type, 1–50 |
GET /v1/{resource}/{id}/links | A record's traceability links, one entry per related record |
POST /v1/links | Link two records: { "source_type", "source_id", "target_type", "target_id" }. Linking twice returns the existing link |
Filtering and pagination
GET /v1/cars?status=open&status=in_progress&target_closure_date[lte]=2026-12-31&limit=50col=valueequals; repeat the parameter for any-of.col[in]=a,bany-of, comma-separated.col[gte]=andcol[lte]=ranges on dates, timestamps and numbers.col[is]=nullorcol[is]=not_null.q=case-insensitive text match over the resource's searchable columns.updated_since=ISO 8601 timestamp.limit=1 to 200, default 50.
Unknown parameters are refused with 422, so a misspelt filter can't silently return everything. Responses are { "data": [...], "next_cursor": "…" | null }; pass cursor= with the same filters for the next page. Pages are ordered newest first and stable while you page.
Errors
{ "error": { "code": "validation_failed", "message": "status must be one of: open, investigating, closed, cancelled." } }| Status | Code | When |
|---|---|---|
| 401 | unauthorized | Missing, unknown, revoked or expired key; creator no longer owner or admin; licence suspended |
| 403 | insufficient_scope / forbidden | A write with a read key / the creator's permissions refuse it |
| 404 | not_found | No such record, or one the key's user can't see. The two are never distinguished |
| 405 | method_not_allowed | The resource doesn't offer that method |
| 409 | conflict / record_locked | Duplicate unique value / the record has been finalised by a person |
| 413, 415 | payload_too_large, unsupported_media_type | Body over 256 KB / not JSON |
| 422 | validation_failed | Bad field, value or type; missing required field; unknown or malformed parameter |
| 429 | rate_limited | See below; honour Retry-After |
| 500, 503 | internal_error, service_unavailable | Something went wrong on our side |
Rate limits
About 120 requests a minute per key. Repeated failed authentications from one address are limited separately. If you get a 429, wait for the number of seconds in the Retry-After header.
Examples
BASE=https://mihqijyqxrggmcesqczp.supabase.co/functions/v1/api/v1
KEY=dq_live_...
curl -s "$BASE/me" -H "Authorization: Bearer $KEY"
# Open CARs due this year
curl -s "$BASE/cars?status=open&status=in_progress&target_closure_date[lte]=2026-12-31" \
-H "Authorization: Bearer $KEY"
# Raise an NCR (number assigned) and a CAR, then link them
curl -s -X POST "$BASE/nonconformities" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Scratched panel","severity":"minor","source":"internal"}'
curl -s -X POST "$BASE/cars" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Panel scratching on line 2","priority":"medium"}'
curl -s -X POST "$BASE/links" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"source_type":"nonconformity","source_id":"<ncr id>","target_type":"car","target_id":"<car id>"}'
# Fill in D2 of the CAR (content keys are merged)
curl -s "$BASE/car-steps?car_id=<car id>&step_number=2" -H "Authorization: Bearer $KEY"
curl -s -X PATCH "$BASE/car-steps/<step id>" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"status":"in_progress","content":{"what":"Scratches on panel face"}}'