API Reference

Import contacts (array)

POST /contacts/batch — bulk-import contacts into a sending domain from a JSON array.

POST/contacts/batch

Import 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.

This is the recommended way to add many contacts. One batch request does the whole import in a single pass — it acquires your account's contact-limit lock once and writes the rows in chunks. Looping POST /contacts once per contact does the opposite: every request queues behind the same per-account lock and holds a database connection while it waits, so a few hundred parallel creates get slow and start failing where one batch call finishes in seconds. If you have more than a handful of contacts, send them here — and if you have more than 10,000, send sequential batches rather than parallel single creates.
Body
domainstringrequired

The 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.

contactsarrayrequired

Array of contact objects to import. You can also send a bare JSON array as the body instead of wrapping it in { contacts: [...] }.

contacts[].emailstringrequired

Email address. Rows with a missing or invalid email are skipped.

contacts[].first_namestringoptional

Optional first name.

contacts[].last_namestringoptional

Optional last name.

contacts[].unsubscribedbooleanoptional

Optional opt-out flag. Defaults to false.

contacts[].propertiesobjectoptional

Optional 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_conflictstringoptional

upsert (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.

Request
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:

Request (audience-scoped)
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.