Async bulk contact import

Search Documentation

Search across all developer documentation

contacts

Async bulk contact import

POST /v1/contacts/bulk
Auth required api.contacts.manage

Submits a batch of up to 5 000 contacts for asynchronous upsert. The endpoint returns immediately with a job id and 202 Accepted; poll GET /v1/contacts/bulk/{job_id} until status is completed or failed.

Each row is processed independently — a single row's validation failure (unknown attribute, malformed email, blank external_id, malformed list id) marks that row failed while the rest of the batch continues. The overall job never returns failed due to per-row errors.

Read the failed rows from GET /v1/contacts/bulk/{job_id}: each carries its index in the submitted array and a reason. A row that fails validation is never written — it reaches neither the contact list nor SES.

422 is reserved for faults in the request as a whole — an unparseable body, an empty contacts array, or more than 5 000 items. A malformed row does not produce a 422, and never costs the rest of the batch.

Supports the Idempotency-Key header: a duplicate key within 24 h replays the original 202.

Request body

Content type: application/json

contacts array<object> required

The contacts to import. Capped at 5000 per request.

Constraints: 1–5000 items

Each entry in contacts:

email string<email> required
external_id string | null optional

Your own identifier for this contact. When present the row resolves exactly as PUT /v1/contacts/by-external-id/{external_id} does — it creates the contact with that id, adopts an existing contact whose external_id is unset, or updates the contact that already carries it. Omit it for the plain email-keyed upsert, which leaves external_id untouched. If the email already belongs to a contact carrying a different external id, only that row fails (outcome: "failed", with a reason naming the conflicting id) — the rest of the batch still applies. Use rebind_external_id to re-key instead of failing.

rebind_external_id boolean optional

When the row's email already belongs to a contact carrying a different external id, the import normally fails that row rather than steal the contact from its current identity. Set this to true to instead rebind the email onto this row's external_id — an authoritative "I own this identity, re-key it" write that heals a stale external id in place. Ignored when external_id is absent. Leave false (the default) unless you are the sole owner of the contact.

Default: false

attributes object optional

Custom attribute map. Validated per-row; a failure marks only that row failed.

unsubscribe_all boolean | null optional

Master opt-out. Omitted-means-untouched — a null/absent value never changes the contact's current opt-out state, so re-importing an existing audience cannot resurrect its unsubscribes. Send false explicitly to re-subscribe a contact, and true to opt them out.

topic_preferences array<object> optional

Each entry in topic_preferences:

topic_name string required

The topic's name (see /v1/topics).

subscription_status string enum required

One of: OPT_IN, OPT_OUT

lists object optional

Inline static-List membership directive carried on POST and PATCH contact writes. add/remove apply a delta; set, when present, replaces membership declaratively (an empty set clears all memberships). List ids must name static Lists in the org.

Fields of lists:

add array<string<uuid>> optional

List ids to add the contact to.

remove array<string<uuid>> optional

List ids to remove the contact from.

set array<string<uuid>> optional

When present, replaces the contact's static-List membership exactly. An empty array clears all memberships. Mutually exclusive with the add/remove delta.

Responses

Errors follow the RFC 7807 problem format — see the error reference.

202 Batch accepted for async processing application/json
job_id string<uuid> required
status string required

Job lifecycle state — pending, running, completed, or failed.

total integer required

Number of contacts accepted for processing.

401 Missing, malformed, or unknown API key application/problem+json
403 Either the credential lacks the required scope (code: invalid_scope), or it is bound to the test environment and this operation is irreversible (code: test_environment_forbidden). Branch on code: the first is fixed by granting the scope, the second only by using a live credential. See the "Live and test credentials" section of the API description. application/problem+json
404 Resource not found application/problem+json
422 Query parameter or path value failed validation application/problem+json
429 Per-org rate limit exceeded application/problem+json
500 Unexpected server-side failure. The code is internal_error. The request_id field can be quoted to SendOps support to investigate. application/problem+json