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

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