Skip to content

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.

X-Workspace-Id
integer

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.

Idempotency-Key
string
<= 255 characters

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.

object
items
required

The rows to upsert. At most 200 per request.

Array<object>
>= 1 items <= 200 items

At least one of name, username, phone, email or externalId is required. The same shape is used for a batch row.

object
platform

Platform for the new contact. Defaults to telegram.

string
default: telegram
Allowed values: telegram twitter
name
string
nullable <= 100 characters
username

Handle with or without a leading @ - the @ is stripped server-side.

string
nullable <= 100 characters
phone
string
nullable <= 20 characters
email
string
nullable <= 200 characters
company
string
nullable <= 200 characters
externalId

Your own customer id. Must be unique inside the workspace; the batch endpoint upserts on it.

string
nullable <= 100 characters
country
string
nullable <= 100 characters
city
string
nullable <= 100 characters
notes
string
nullable <= 500 characters
stage
string
nullable
Allowed values: novalue lead conversation proposal negotiation won lost
pipelineStageId

Column on a custom pipeline board. Must belong to a board in the active workspace.

integer
nullable
leadScore
integer
nullable <= 100
source

Where the record came from (Manual, CsvImport, RevenueImport, …). Defaults to the historical v1 value.

string
nullable
tags

Tag names. Missing tags are created in the workspace dictionary.

Array<string>
nullable <= 50 items
customFields

Custom field values keyed by field key. A null value clears that key.

object
matchOn

Which field identifies an existing contact. none always creates. Email and username are matched case-insensitively.

string
default: externalId
Allowed values: externalId email phone username none
continueOnError

When false the run stops at the first failing row and the remainder are reported as skipped. Rows already written stay written.

boolean
default: true
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"
}
]
}

Every row succeeded.

object
items
Array<object>
object
index

Position of this row in the request items array.

integer
id

The contact id, or null when the row failed.

integer
nullable
externalId
string
nullable
status
string
Allowed values: created updated failed skipped
error

Why the row failed. Null on success.

string
nullable
total
integer
created
integer
updated
integer
failed
integer
skipped
integer

Partial success. Inspect items[].status and items[].error for the rows that did not land.

object
items
Array<object>
object
index

Position of this row in the request items array.

integer
id

The contact id, or null when the row failed.

integer
nullable
externalId
string
nullable
status
string
Allowed values: created updated failed skipped
error

Why the row failed. Null on success.

string
nullable
total
integer
created
integer
updated
integer
failed
integer
skipped
integer
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
error

Machine-readable error code.

string
Allowed values: bad_request not_found conflict unauthorized forbidden payment_required provider_rejected rate_limited internal_error
message

Human-readable explanation of the error.

string
details

Optional structured context (field-level validation errors, etc.).

nullable
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
error

Machine-readable error code.

string
Allowed values: bad_request not_found conflict unauthorized forbidden payment_required provider_rejected rate_limited internal_error
message

Human-readable explanation of the error.

string
details

Optional structured context (field-level validation errors, etc.).

nullable
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
error

Machine-readable error code.

string
Allowed values: bad_request not_found conflict unauthorized forbidden payment_required provider_rejected rate_limited internal_error
message

Human-readable explanation of the error.

string
details

Optional structured context (field-level validation errors, etc.).

nullable
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
error

Machine-readable error code.

string
Allowed values: bad_request not_found conflict unauthorized forbidden payment_required provider_rejected rate_limited internal_error
message

Human-readable explanation of the error.

string
details

Optional structured context (field-level validation errors, etc.).

nullable
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
error

Machine-readable error code.

string
Allowed values: bad_request not_found conflict unauthorized forbidden payment_required provider_rejected rate_limited internal_error
message

Human-readable explanation of the error.

string
details

Optional structured context (field-level validation errors, etc.).

nullable
Example
{
"error": "rate_limited",
"message": "rate limit exceeded"
}
X-RateLimit-Limit
integer

Maximum requests allowed per minute for this key.

X-RateLimit-Remaining
integer

Requests remaining in the current window.

X-RateLimit-Reset
integer

Unix timestamp (seconds) when the rate limit window resets.