Conventions
Ids, dates, option values, errors, idempotency and retries — the rules every endpoint follows.
Requests and responses
- JSON in, JSON out. Send
Content-Type: application/jsononPOSTandPUT, except the attachment upload which ismultipart/form-data. - Every endpoint lives under
/v1. Nothing under it will be removed or renamed; see the changelog for how changes are made. GET /healthis unauthenticated and returns{ "status": "ok" }. It is there for monitoring; it does not check the database.
Every write endpoint's page lists its fields in a table with a Required column. Anything not
marked required can be left out, and the notes say what happens when it is. PATCH bodies never
require a particular field, only at least one.
Ids
Every record is identified by a UUID string, returned as id. Numeric ids are never exposed and
never accepted. Ids are stable for the life of the record; names are not, since staff can rename
things in the CRM. Store ids.
Dates and timezones
The API works in your organisation's timezone, the one set in the CRM's organisation settings
(Europe/London for UK customers). Storage is always UTC; the zone decides how dates are read
from you and shown to you. GET /v1/whoami says which zone is in effect. To use another one for a
request, send X-Timezone with an IANA name, e.g. X-Timezone: Europe/Dublin; an unknown name is
a 400.
Every timestamp you receive carries the zone's offset for that instant:
2026-07-10T09:00:00.000+01:00 a July morning, British Summer Time
2026-12-10T09:00:00.000+00:00 a December morning, GMTBoth are what a colleague sees in the CRM. Any ISO 8601 parser reads them correctly, and the UTC instant is unchanged.
Every date you send without an offset is read in the zone. "2026-07-10T09:00" means 9am
BST and is stored as 08:00 UTC; "2026-12-10T09:00" means 9am GMT. The offset is worked out
for the date you send, not for today, so a July time sent in December is still BST. A value with
Z or an explicit offset is taken exactly as written.
A bare date is midnight in the zone. "2026-07-10" is read as 00:00 on 10 July in your
timezone (23:00 UTC the evening before, in summer), which is exactly what the CRM's own date
pickers store, so the CRM and the API agree which day it is. It comes back as
2026-07-10T00:00:00.000+01:00. Fields staff usually set by date — expectedCloseDate, and
soldAt, lostAt, onHoldAt — are still full timestamps, because the CRM also sets them
automatically with a real time when a stage changes. Read them by their date part.
Accepted input forms: YYYY-MM-DD, YYYY-MM-DDTHH:MM, YYYY-MM-DDTHH:MM:SS, optionally with
milliseconds and Z or ±HH:MM. Anything else, including 10/07/2026, is a 422 saying so.
Option values
Fields that map to a dropdown in the CRM (leadType, marketing, country, assignedTo, and
so on) accept either the option's id or its name. Names match case-insensitively and ignore
surrounding whitespace. A name that matches more than one option is a 422 asking for the id.
GET /v1/leads/fields lists every option with both.
An empty string means "not set", the same as leaving the field out, because HTML forms send ""
for an unselected dropdown.
Booleans
true, "true", "yes", "y", "1", "on" and 1 are all true. Anything else is false.
Errors
Every error has the same shape:
{
"error": "Invalid lead payload",
"fields": {
"email": "Must be a valid email address",
"leadType": "No lead.type option named \"Windows & Doors\""
}
}error is a sentence for a human. fields, when present, is keyed by the offending property or
query parameter and says what to change. Some responses add a hint.
| Status | Meaning | What to do |
|---|---|---|
| 200 | Done. On a create with a reference already seen, created: false | — |
| 201 | Created | — |
| 400 | The body could not be read (malformed JSON or multipart) | Fix the request |
| 401 | Key missing, invalid or expired | See Authentication |
| 403 | The key is valid but this action is not allowed on this record | Read the error |
| 404 | No such record in your organisation, or no such route | Check the id |
| 413 | A file or the request is over the size limit | Send less |
| 415 | Wrong Content-Type | Read the endpoint's page |
| 422 | The request was understood but is invalid | Fix the field(s) named in fields |
| 429 | Too many requests | Wait for Retry-After seconds |
| 500 | Our fault | Retry later; we are alerted automatically |
| 502 | The CRM behind the API did not answer | Safe to retry — see below |
| 503 | A feature is not enabled on this environment, or a query took too long | Read the error |
Idempotency and retries
Network calls fail. The API is designed so that retrying is always safe:
POST /v1/leadsis idempotent onreference. Sending the same reference again returns the original lead with200andcreated: false. Nothing is duplicated.POST /v1/leads/{id}/attachmentsaccepts an optionalIdempotency-Keyheader. A replay with the same key returns the original files without storing anything.PUT /v1/leads/{id}/stageto a stage the lead is already in returnschanged: false.DELETEof something already deleted is a 404, which your code can treat as success.
A 502 means the API could not reach the CRM. The request was not applied; repeat it as sent. A 500 may or may not have been applied, which is why the operations above are built so that repeating them is harmless.
Audit trail
Everything the API changes is recorded in the CRM's audit log exactly as a staff change would be: lead and customer creation, every field a patch touches, stage moves, assignments, and files added or removed. Each entry is marked as made through the API, shows which API key made it by name, and is attributed to the user who created that key. Name keys after the integration they belong to, so staff can read the log.
The same goes for the CRM's search: every record the API creates or changes is re-indexed at once. Endpoint pages take both for granted and do not repeat it.
Caching
Reference data (/v1/leads/fields, /v1/users) is served with Cache-Control: private, max-age=300. Fetch it when your integration starts and after any 422, not on every request.
Everything else is no-store.
Rate limits
None are enforced today. Design for 60 requests per minute per key; when limiting is switched on
it will be announced in the changelog and enforced with 429 and Retry-After.
Test data
test: true on a lead marks it as test data in the CRM and excludes it from reporting. Lists
exclude test leads unless you ask for them with test=include or test=only.