Bulk create or update contacts
POST /v1/contacts/batch
Upserts up to 200 contacts in one request - the first call an e-commerce or
accounting integration makes. Each row is matched against the active
workspace on matchOn (default externalId); a match updates that contact,
no match creates one.
Rows are committed one at a time, so a single bad record cannot roll the
whole import back. The response reports every row’s outcome by index. The
status is 200 when every row succeeded and 207 when some did not.
On an update only the properties present in the row are written, so a
partial export cannot blank out fields it does not know about.
customFields merges into the existing values; tags are added, never
removed. Emits contact.created / contact.updated per row. Send
Idempotency-Key to make a retry of the whole batch safe. Requires scope
contacts:write.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Header Parameters
Section titled “Header Parameters ”Act on this workspace’s shared CRM pool. Membership is verified on every request, so an id you do not belong to falls back to personal scope rather than widening access. Omit it to use your default workspace. Applies to every CRM endpoint, not only the ones that list it here.
A unique string of your choosing (a UUID works). The first request with this key runs normally and its response is stored for 24 hours; a retry with the same key returns that stored response verbatim, with Idempotency-Replayed: true, instead of writing again. Reusing a key with a different body returns 409.
Request Body required
Section titled “Request Body required ”object
The rows to upsert. At most 200 per request.
At least one of name, username, phone, email or externalId is required. The same shape is used for a batch row.
object
Platform for the new contact. Defaults to telegram.
Handle with or without a leading @ - the @ is stripped server-side.
Your own customer id. Must be unique inside the workspace; the batch endpoint upserts on it.
Column on a custom pipeline board. Must belong to a board in the active workspace.
Where the record came from (Manual, CsvImport, RevenueImport, …). Defaults to the historical v1 value.
Tag names. Missing tags are created in the workspace dictionary.
Custom field values keyed by field key. A null value clears that key.
object
Which field identifies an existing contact. none always creates. Email and username are matched case-insensitively.
When false the run stops at the first failing row and the remainder are reported as skipped. Rows already written stay written.
Example
{ "matchOn": "externalId", "continueOnError": true, "items": [ { "externalId": "shopify-cust-88213", "name": "Ada Lovelace", "email": "ada@example.com", "company": "Analytical Engines", "tags": [ "vip" ] }, { "externalId": "shopify-cust-88214", "name": "Charles Babbage", "email": "charles@example.com" } ]}Responses
Section titled “ Responses ”Every row succeeded.
object
object
Position of this row in the request items array.
The contact id, or null when the row failed.
Why the row failed. Null on success.
Partial success. Inspect items[].status and items[].error for the rows that did not land.
object
object
Position of this row in the request items array.
The contact id, or null when the row failed.
Why the row failed. Null on success.
Example
{ "items": [ { "index": 0, "id": 4821, "externalId": "shopify-cust-88213", "status": "updated", "error": null }, { "index": 1, "id": null, "externalId": "shopify-cust-88214", "status": "failed", "error": "at least one of name, username, phone, email, externalId is required" } ], "total": 2, "created": 0, "updated": 1, "failed": 1, "skipped": 0}The request body or parameters failed validation.
Standard error envelope for all v1 error responses.
object
Machine-readable error code.
Human-readable explanation of the error.
Optional structured context (field-level validation errors, etc.).
Example
{ "error": "bad_request", "message": "at least one of name, username, phone is required"}Missing or invalid bearer token.
Standard error envelope for all v1 error responses.
object
Machine-readable error code.
Human-readable explanation of the error.
Optional structured context (field-level validation errors, etc.).
Example
{ "error": "unauthorized", "message": "Invalid bearer principal"}The API key does not have the required scope for this operation.
Standard error envelope for all v1 error responses.
object
Machine-readable error code.
Human-readable explanation of the error.
Optional structured context (field-level validation errors, etc.).
Example
{ "error": "forbidden", "message": "scope contacts:write is required"}The request collides with existing state: a duplicate externalId, an Idempotency-Key replayed with a different body or still in flight, or a permanent delete blocked by records that still reference the contact.
Standard error envelope for all v1 error responses.
object
Machine-readable error code.
Human-readable explanation of the error.
Optional structured context (field-level validation errors, etc.).
Example
{ "error": "conflict", "message": "a contact with externalId 'shopify-cust-88213' already exists", "details": { "existingContactId": 4821 }}Per-key rate limit exceeded. Retry after the time indicated by X-RateLimit-Reset.
Standard error envelope for all v1 error responses.
object
Machine-readable error code.
Human-readable explanation of the error.
Optional structured context (field-level validation errors, etc.).
Example
{ "error": "rate_limited", "message": "rate limit exceeded"}Headers
Section titled “Headers ”Maximum requests allowed per minute for this key.
Requests remaining in the current window.
Unix timestamp (seconds) when the rate limit window resets.