Skip to main content
One uniform surface covers every entity — built-in ones like leads and deals, and anything the tenant has defined themselves.
{object} is whatever GET /v1/objects returned — leads, or object:property (URL-encoded as object%3Aproperty). Read the object model first. The field names, types and which ones are required all come from the object’s schema, and they differ per tenant.

Listing and paging

To get the next page, pass next_cursor back:
When next_cursor is null you have reached the end. That is the only signal you need — do not compare counts. limit is 1–100 and defaults to 25. Out-of-range values are clamped rather than rejected.

Treat the cursor as opaque

It is a base64 blob and its contents are not part of the contract. Do not parse it, increment it, or build one yourself. The reason it exists in this form: paging is anchored to a record, not to a position in the list. So if records are created or deleted while you are paging, you still will not see the same record twice or miss one. Offset paging (?page=2) cannot promise that — one insert shifts every row down and the last row of page 1 silently reappears at the top of page 2. If you are syncing data, that guarantee is the difference between a clean sync and duplicated records.

Creating

Idempotency-Key is required. Send a unique value per logical create and reuse it on retries: if the first attempt actually succeeded but the response never reached you, retrying with the same key returns the original record instead of creating a second one. Omitting the header returns 428. Built-in fields and custom fields are sent the same way, flat in the body. You do not need to know which is which.

Updating

PATCH is partial: omit what you are not changing. Custom fields are merged, not replaced — patching one custom field leaves the others intact.

Validation

A bad payload returns 422, and the response names every field that is wrong:
All problems are reported at once, so you are not fixing one field per round trip. Things worth knowing:
  • Unknown fields are rejected, not ignored. A typo fails loudly instead of silently dropping your data.
  • id, created_at and updated_at are read-only. Sending them is an error rather than a no-op, so you never believe you set an id you did not.
  • required is per tenant. A field that is optional for one customer can be mandatory for another. Read the schema; do not assume.
  • Numeric strings ("42") are accepted for number fields. "yes" is not a boolean; true and "true" are.

Creating many at once

Up to 100 items per request, as a bare array or { "records": [...] }. Idempotency-Key is required, as it is for a single create.

Partial success is normal — always read failed

The response is 200 even when some items failed. One bad row in a nightly import should not throw away the other 99 — but it does mean the HTTP status alone no longer tells you whether everything was written. failed is always present (empty when nothing failed), so check it every time. index is the position in the array you sent. It is the only way to find the offending row in your own data, since a record that failed to be created has no id.

What fails the whole request instead

These return a 4xx and write nothing:

Duplicates within a batch

If the workspace has duplicate checking configured, items that duplicate an earlier item in the same batch on the configured fields are reported as DUPLICATE_IN_BATCH instead of being written. The first occurrence wins, and the message tells you which index it collided with. This only looks within the request. It is not a check against records already in the CRM.

Retrying

Reuse the same Idempotency-Key when you retry a batch that timed out. The replay returns without creating anything new. If some items failed on validation, fix those rows and send them as a new batch with a new key — reusing the old key replays the old outcome rather than processing your corrections.

A note for SDK authors

Wrap this so callers cannot ignore failed. Returning a plain response object invites if (res.success) and silent data loss. Prefer either raising on any failure, or returning a result type that forces the caller to handle both lists.

Filtering and sorting

Sorting

Comma-separated field names, at most 3. A leading - means descending. Only fields the object’s schema publishes can be sorted on; anything else is a 400 naming the field.

Filtering

filter takes a JSON object, URL-encoded. A single condition:
Combine with and / or, nested at most 3 levels deep:
Values must match the field’s type. The schema tells you what each field is — priority on leads is a number, so {"op":"eq","value":"High"} is a 400, not a silent empty result. This is checked before the query runs, so a type mistake never reaches the database. Unknown fields are a 400 naming the field, rather than being ignored. An ignored filter would return more rows than you asked for and look successful, which is the worst possible failure for a sync. Values are always treated as literal text, never as query syntax. Commas, dots, parentheses, quotes and SQL fragments in a value match themselves and nothing else — searching for 50% finds the string “50%”, not “50 followed by anything”.

Filtering on custom fields

Custom fields work the same way. Because they are stored as JSON, comparisons on them are textual — a custom number field sorts and compares lexically ("10" < "9"). If ordering matters on a custom numeric field, pad the values or keep it in a built-in field.

Notes, tasks and activities on a record

These work for any object, built-in or custom. Everything you create here shows up on the record inside the CRM — the API writes to the same tables the app reads, which differ per object behind the scenes.

Notes

Only content is required.

Tasks

Creating a task also creates its link to the record, so it appears on that record’s task list in the app — not just in a global task list. The task is owned by and attributed to the user your API key belongs to. A key with no owning user cannot create tasks or notes; re-create the key from a user account.

Activities

Read-only here. Activities are logged by the CRM’s own workflows; this endpoint lets you read the interaction history for a record. Use /v1/activity-types to discover the types a workspace has.

Errors

Same rules as the record itself: a record you cannot see returns 404 for its notes, tasks and activities too — including on write. There is no way to attach anything to a record you could not have read.

Errors, and the 403/404 distinction

The difference between 403 and 404 is deliberate and useful:
  • 404 — you cannot see it. Either it does not exist, or it belongs to another tenant, or it falls outside your key’s data scope (own / team / branch). These are indistinguishable on purpose: telling them apart would confirm that someone else’s record exists.
  • 403 — you can see it, but you are not allowed to do that to it. For example reading a lead works, but deleting it needs leads:delete on your key.
So: 404 → check the id and your data scope. 403 → add a scope to your key.

Suggested sync loop

  1. GET /v1/objects to discover what you can access.
  2. GET /v1/objects/{object}/schema for each object you touch, and cache it.
  3. Page through records with cursor until next_cursor is null.
  4. Write with Idempotency-Key, retrying the same key on network failure.
  5. Re-fetch the schema when a write starts failing with 422 — a tenant may have added a mandatory field.