Async bulk contact import
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.
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.
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 code is internal_error. The
request_id field can be quoted to SendOps support to investigate.
application/problem+json