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.mailblastr.com/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 net-new contacts; updated counts existing contacts that were merged; skipped counts rows that were invalid or left untouched due to on_conflict: skip; total is the number of valid, email-deduplicated rows that were processed — rows rejected for a missing or invalid email are counted in skipped only, and duplicate emails inside one payload collapse into a single row (the last one wins), so total is not the sum of the other three.
The whole batch is checked against your plan's contact limit once, for the number of valid rows it contains. If the batch would take you past the limit it is rejected with 429 contact_limit_reached and nothing is written — see Errors. Split the import into smaller batches, or upgrade, and re-send.
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 { Mailblastr } from 'mailblastr';
const mb = new Mailblastr('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.