Skip to main content

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 URLhttps://mihqijyqxrggmcesqczp.supabase.co/functions/v1/api/v1
OpenAPI 3.1https://mihqijyqxrggmcesqczp.supabase.co/functions/v1/api/v1/openapi.json (no key needed)
AI assistantsUse 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

ScopeAllows
readGET only
read_writeGET, 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:

ResourceNot writable
technical-reviewsconclusion, reviewer, status complete; a concluded review is final
management-reviewsapproval and status minutes_approved; approved minutes are final
controlled-documentsapprover, approval date, status approved
effectiveness-checksresult, assessor and observed value
standard-mappingsstatus and confirmation; mappings are created as proposed
claimsstatus supported
auditslock fields; a locked audit is read-only
evidenceeverything (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.

ResourceMethodsNotes
processesGET POST PATCH DELETEparent_id builds the tree
auditsGET POST PATCH DELETElocked audits are read-only
audit-findingsGET POST PATCH DELETE
carsGET POST PATCH DELETEPOST assigns car_number and creates the nine 8D steps; proposed CARs move only to open
car-stepsGET PATCHonly content (merged key by key) and status; completing records who and when
actionsGET POST PATCH DELETE
evidenceGETmetadata only
nonconformitiesGET POST PATCH DELETEncr_number assigned when omitted
suppliersGET POST PATCH DELETE
supplier-evaluationsGET POST PATCH DELETEcounts, never percentages
risksGET POST PATCH DELETEreference assigned when omitted; score calculated
quality-objectivesGET POST PATCH DELETE
kpis, kpi-measurementsGET POST PATCH DELETE
effectiveness-checksGET POST PATCH DELETEresult recorded by a person
management-reviewsGET POST PATCH DELETEminutes approved by a person
personnel, competencies, training-recordsGET POST PATCH DELETEpersonnel are not the same as Doctilla users
controlled-documentsGET POST PATCH DELETErevisions approved by a person
standards, standard-clausesGET POST PATCH DELETEimport only text you hold a licence for
standard-mappingsGET POST PATCH DELETEcreated as proposed only
products, product-configurations, components, claims, publications, product-changes, technical-reviewsGET POST PATCH DELETEProduct 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/meThe organisation, acting user, their role and the key's scope
GET /v1/search?q=…&types=car,nonconformity&limit=10Text search across record types; limit is per type, 1–50
GET /v1/{resource}/{id}/linksA record's traceability links, one entry per related record
POST /v1/linksLink 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=50
  • col=value equals; repeat the parameter for any-of.
  • col[in]=a,b any-of, comma-separated.
  • col[gte]= and col[lte]= ranges on dates, timestamps and numbers.
  • col[is]=null or col[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." } }
StatusCodeWhen
401unauthorizedMissing, unknown, revoked or expired key; creator no longer owner or admin; licence suspended
403insufficient_scope / forbiddenA write with a read key / the creator's permissions refuse it
404not_foundNo such record, or one the key's user can't see. The two are never distinguished
405method_not_allowedThe resource doesn't offer that method
409conflict / record_lockedDuplicate unique value / the record has been finalised by a person
413, 415payload_too_large, unsupported_media_typeBody over 256 KB / not JSON
422validation_failedBad field, value or type; missing required field; unknown or malformed parameter
429rate_limitedSee below; honour Retry-After
500, 503internal_error, service_unavailableSomething 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"}}'