Import contacts (array)
POST /contacts/batch — bulk-import contacts into a sending domain from a JSON array.
/contacts/batchImport up to 10,000 contacts into one of your sending domains in a single request by sending a JSON array. Contacts are domain-scoped, so pass the domain they belong to alongside the rows. Invalid rows (missing or malformed email) are counted as skipped and never cause the whole request to fail.
domainstringrequiredThe sending domain these contacts belong to (one of your domains, e.g. yourdomain.com). Can also be passed as a query parameter — ?domain=yourdomain.com — which is what you want when the body is a bare JSON array. A missing domain, or one that is not yours, returns 422 validation_error.
contactsarrayrequiredArray of contact objects to import. You can also send a bare JSON array as the body instead of wrapping it in { contacts: [...] }.
contacts[].emailstringrequiredEmail address. Rows with a missing or invalid email are skipped.
contacts[].first_namestringoptionalOptional first name.
contacts[].last_namestringoptionalOptional last name.
contacts[].unsubscribedbooleanoptionalOptional opt-out flag. Defaults to false.
contacts[].propertiesobjectoptionalOptional map of custom property key/value pairs. Unlike POST /contacts, this endpoint does not check keys against your contact-property registry — any key matching \w{1,64} is accepted and stored (up to 50 keys per contact, values truncated at 1000 characters). Unregistered keys are written to the contact but have no declared type or fallback, so register them with POST /contact-properties if you want them usable as merge tags.
on_conflictstringoptionalupsert (default) — update the existing contact when the email already exists. skip — leave the existing contact untouched. Can also be passed as a query parameter, ?on_conflict=skip, for use with a bare JSON array body.
const res = await fetch('https://www.sendping.co/api/contacts/batch', {
method: 'POST',
headers: {
'Authorization': 'Bearer mb_xxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"domain": "yourdomain.com",
"contacts": [
{ "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak" },
{ "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace" }
]
}),
});
const data = await res.json();
console.log(data);Response
{
"object": "contact_import",
"imported": 1,
"updated": 1,
"skipped": 0,
"total": 2
}imported counts new contacts; updated counts existing contacts merged by email. skipped includes invalid email rows and conflicts skipped by request. total counts source rows including duplicates, so it need not equal the sum of the other counts. limit_skipped and system_skipped are retained for compatibility and are zero: stored contacts are unlimited.
All plans support unlimited stored contacts. Each import is serialized and committed atomically. The JSON batch API accepts up to 10,000 rows per request; larger CSVs use the direct-S3 upload and preview flow.
Audience-scoped variant
The nested route POST /audiences/:audience_id/contacts/batch imports the same JSON array into one specific audience instead of a domain — no domain field in the body. Everything else (the 10,000-row limit, on_conflict, the response, the contact-limit behaviour) is identical:
import { SendPing } from 'sendping';
const mb = new SendPing('mb_xxxxxxxxx');
const { data, error } = await mb.contacts.batch({
audienceId: 'AUDIENCE_ID',
"contacts": [
{ "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak" },
{ "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace" }
]
});
console.log({ data, error });An empty or entirely-invalid contacts array returns 422 validation_error. More than 10,000 rows per request is rejected with 422 validation_error. On the domain-scoped route a missing domain, a domain that is not one of yours, or an audience_id in the body returns 422 validation_error; on the audience-scoped route an unknown audience returns 404 not_found. See Errors.