Request and response formats#
The API speaks JSON over HTTPS. Requests send JSON bodies where a body is needed, and responses come back as JSON with camelCase property names. This page covers the conventions that hold across every endpoint - the methods, the shape of a collection, how to select fields, how to read changes incrementally, the status codes, and the error format.
HTTP methods#
The API uses the standard methods. A read names a thing, not an action performed on it; an action that is not a plain create, replace, or delete is a POST to a named step on the resource (for example notification/dispatches/{dispatchId}/approve).
| Method | Use |
|---|---|
GET |
Read a collection or a single resource |
POST |
Run a query that needs a request body, or perform an action such as preparing a dispatch |
PUT |
Replace or configure a resource |
DELETE |
Remove or unconfigure a resource |
Two conventions are worth knowing. A POST that queries a collection carries a /search suffix (for example certification/tickets/search), which leaves a bare POST to a collection free to mean "create" for as long as this API lives. A report chooses its rendering with a ?format= parameter rather than a /search suffix, because choosing a format commissions an artifact rather than filtering a set. Most reports are a GET; reporting/compliance-summaries is a POST, because its filters are too large for a query string.
Request bodies#
Send a request body as JSON with Content-Type: application/json. Property names may be camelCase; where a request names fields to include, the match is case-insensitive, so either camelCase or PascalCase works.
Also send:
Authorization: Bearer <secret>orX-Api-Key: <key>to authenticate (see Authentication).X-Company: <organization-handle>to name your organization, unless your credential already carries it.Accept: application/json(assumed when absent).
Response bodies#
A response body is JSON with camelCase property names.
A directory collection returns a plain JSON array. The directory/* collections (directory/accounts, directory/members, directory/affiliations, and so on) return no envelope and no file attachment - the array is the whole response. A projected collection omits any property whose value is null, so a field that is absent from a row is null rather than an error.
A search collection returns one page in an envelope. The search collections - certification/plans, certification/assignments, certification/achievements, certification/tickets, competency/competencies, competency/validations, competency/profiles, competency/designations, training/courses, and training/enrolments - return an object with four properties:
items holds the rows on this page and total counts the rows across every page. Ask for a page with the page (1-based) and pageSize query parameters. pageSize defaults to 25 and is capped at 200. An out-of-range value is corrected rather than rejected: a page below 1 reads as the first page, a page size below 1 reads as 25, and a page size above 200 reads as 200.
A search collection can also be ordered with sort (the property to order by; each collection documents its own keys) and direction (desc for descending; anything else is ascending).
A single resource returns a JSON object. For example, directory/companies/{company} answers with one object rather than an array, because a caller looks up a specific organization by id.
Apart from the search envelope, there is no wrapping metadata object, no data key, and no status field inside the body - the HTTP status carries that.
Selecting fields#
Pass filter.includes to restrict a collection to the columns you want, in the order you want them:
Matching is case-insensitive. The order you list is the order you get. If none of the names match a real column - usually a typo - the response is an array of empty objects rather than every column, so the mistake fails loudly instead of quietly shipping a far larger payload than you asked for.
Reading changes incrementally#
Directory collections are not paged. A directory/* endpoint returns the entire collection. That is what a caller building a mirror wants. Know what it means at scale: against production data, directory/members returns roughly 111,000 rows and directory/affiliations roughly 239,000.
The pattern is to mirror once, then read only what has moved since. Pass lastChangeTimeSince as an inclusive lower bound on a row's last change time:
Hold your mirror, record the newest change time you have seen, and bound the next read with it rather than downloading the whole collection again.
Reports and file formats#
The compliance reporting endpoint chooses its rendering with a ?format= query parameter. The filters stay in the request body and the format stays in the URL, so you can change how the answer arrives without touching what you asked for.
| Format | Answer |
|---|---|
json |
The rows, as a JSON array. The default when format is omitted |
csv |
A text/csv attachment, one row per member per department per measurement |
xlsx, pdf |
501 Not Implemented until a renderer exists - named rather than rejected, because they are intended |
| anything else | 400 Bad Request, naming the formats that are available |
The CSV leads with a UTF-8 byte order mark so a spreadsheet reads accented names correctly, and any field starting with =, +, -, or @ is quote-prefixed so a spreadsheet does not treat caller-supplied data as a formula.
Status codes#
| Code | Meaning |
|---|---|
200 OK |
The request succeeded and the body carries the result |
204 No Content |
The request succeeded and there is no body (for example, a DELETE) |
400 Bad Request |
The request is malformed or unbounded, names an unknown organization or report format, or lacks an X-Company header where one is required |
401 Unauthorized |
No valid credential |
403 Forbidden |
Authenticated, but not entitled - not a member of the organization, not an operator, or lacking report access |
404 Not Found |
The resource does not exist, or you may not see it (a single-resource read you are not entitled to answers 404, not 403, so its existence is not revealed) |
409 Conflict |
The request collides with current state - for example, preparing a dispatch while one is already waiting |
422 Unprocessable Content |
The request is well formed but cannot be carried out as asked - for example, a notification rule that fails verification |
429 Too Many Requests |
You have exceeded a rate limit; see Rate limits and throttling |
501 Not Implemented |
A named but unbuilt capability, such as a report format without a renderer |
502 Bad Gateway |
A system the endpoint relays to, such as CMDS V4 or Vimeo, refused or failed the request |
503 Service Unavailable |
A dependency the endpoint needs is not available in this deployment |
Errors#
Error responses follow RFC 9457 (which replaced RFC 7807) - the application/problem+json format. The body carries a human-readable summary and, on some errors, a machine-readable discriminator:
{
"type": "https://httpstatuses.io/400",
"title": "Unknown company.",
"status": 400,
"detail": "'acme' is not a registered company.",
"code": "unknown-company"
}
| Field | Description |
|---|---|
type |
A URI identifying the problem type |
title |
A short, human-readable summary of the problem |
status |
The HTTP status code, repeated in the body |
detail |
A human-readable explanation specific to this occurrence |
instance |
A URI for this specific occurrence, when present |
code |
A stable, machine-readable discriminator on the errors that carry one (for example unknown-company, no-company-access) |
Match on status and code rather than on the text of title or detail, which may be reworded. The type URI varies by error, and some problem bodies also carry a traceId.
A 401 from the authentication gate is a problem like any other. When the request named its organization, it also carries a loginUrl member, the sign-in path a browser session should follow.
One kind of refusal is plain application/json rather than problem+json: a refusal from an operator-only or service-key-only endpoint is { "error": "Operator access required." } or { "error": "Service-key access required." }.
Headers#
| Header | Direction | Purpose |
|---|---|---|
Authorization |
Request | Bearer credential (Bearer vsk_...) |
X-Api-Key |
Request | Shared service key, or a personal secret |
X-Company |
Request | The organization handle, unless the credential carries it |
Content-Type |
Request, response | application/json for JSON bodies; text/csv for a CSV report |
Accept |
Request | application/json, assumed when absent |