Response envelope
Every v1 response uses the same success or error envelope — including the nested data.data shape that list endpoints return.
Every v1 response — success or failure — uses one of exactly two shapes. A client that handles both handles the whole API.
Success
{
"success": true,
"data": {}
}Error
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "API access is not enabled for this workspace."
}
}code is a stable machine-readable string; branch on it. message is human-readable and may be reworded — do not match on it. Some errors carry an extra details field with validation specifics.
The full code-to-status table is on Errors.
List endpoints nest their data
All three v1 endpoints are cursor-paginated lists, so their data is the pagination object — which itself contains a data array:
{
"success": true,
"data": {
"data": [
{ "id": "…", "createdAt": "2026-08-08T09:12:44.000Z" },
{ "id": "…", "createdAt": "2026-08-07T16:03:01.000Z" }
],
"nextCursor": "1f6b1d64-6a5f-4f0e-9b0e-b1b1a0c3d2e4"
}
}That is why reading a list is body.data.data, and the page terminator is body.data.nextCursor. It looks redundant and it is deliberate: the envelope belongs to the transport, the inner object belongs to pagination, and every endpoint therefore reads identically.
const body = await res.json();
if (!body.success) throw new Error(body.error.code);
const rows = body.data.data;
const next = body.data.nextCursor; // null on the last pageSee Pagination for walking every page.
Timestamps and ids
- Timestamps are ISO 8601 strings in UTC.
- Ids are UUIDs.
- A field with no value is
nullrather than omitted — for example a lead'sownerwhen nobody is assigned.
HTTP status
The status code always agrees with the envelope: 2xx with "success": true, 4xx or 5xx with "success": false. You can branch on either, but the error.code is the more precise signal.
Where to go next
Last updated