API Reference

Import contacts (CSV)

POST /audiences/:audience_id/contacts/import — bulk-import contacts from a CSV.

POST/audiences/:audience_id/contacts/import

Import up to 10,000 contacts (max 5 MB) from a CSV. The CSV can be sent as a JSON body field or as a raw text/csv / text/plain body. A header row is optional — if present, column names are matched to contact fields (email, first_name, last_name, unsubscribed). By default, any other columns are automatically registered as custom properties (so company, plan, … survive and become {{merge}} tags). Pass ?create_properties=false on the query string for strict mode, where only columns matching an already-registered property are kept and the rest are reported in ignored_columns.

Body
csvstringrequired

CSV text. The email column is always required. Other columns map to contact fields or custom properties. You can also POST a raw text/csv body instead of a JSON wrapper.

on_conflictstringoptional

upsert (default) — update the existing contact when the email already exists. skip — leave it untouched. Can also be passed as a query parameter ?on_conflict=skip when using a raw CSV body.

Query parameters
create_propertiesbooleanoptional

Read from the query string only — a create_properties field in the JSON body is ignored. Defaults to true, so non-builtin CSV columns are auto-registered as string custom properties (up to 50 new per import) and no data is silently dropped. Pass ?create_properties=false to keep only already-registered columns.

segment_idstringoptional

Read from the query string only. Also add every imported email to this segment. The segment must be one of yours and must belong to the audience you are importing into — otherwise the request is rejected with 422 validation_error before any contact is written. When set, the response carries a segment_added count.

Request
import { Mailblastr } from 'mailblastr';

const mb = new Mailblastr('mb_xxxxxxxxx');

const { data, error } = await mb.contacts.import({
  audienceId: 'AUDIENCE_ID',
  "csv": "email,first_name,last_name\nsteve@example.com,Steve,Wozniak\nada@example.com,Ada,Lovelace"
});
console.log({ data, error });

Response (201)

{
  "object": "contact_import",
  "imported": 1,
  "updated": 1,
  "skipped": 0,
  "total": 2,
  "invalid_rows": 0,
  "limit_skipped": 0,
  "system_skipped": 0,
  "ignored_columns": [],
  "source_file": {
    "file_name": "contacts.csv",
    "storage_key": "contact-imports/9f3c1d7a4b2e6058c1d9e2f4/2026/06/23/20260623T172243000Z-7d3f0c1a-5b8e-4a92-9c07-1f6d2e4b8a35/contacts.csv",
    "archived": true
  },
  "contact_limit": {
    "plan": { "id": "free", "name": "Free" },
    "used_before": 12,
    "limit": 1000,
    "remaining_before": 988,
    "remaining_after": 987,
    "limit_skipped": 0,
    "reached": false,
    "message": "Your Free plan allows 1,000 contacts. You had 12, so 988 new contact slots were available. Added 1 new contact and updated 1 existing contact. The complete original CSV is safely archived."
  }
}

imported counts net-new contacts; updated counts existing contacts merged by email; skipped is the roll-up of every row that was not written — invalid_rows (missing or malformed email) plus conflict-skipped rows plus limit_skipped (valid rows dropped because your plan's contact limit was reached) plus system_skipped (rows beyond the 300,000-per-request ceiling that applies only to plans with custom, unmetered contact capacity). total is the number of source rows read from the CSV, counting duplicates, so it is not the sum of the other three: a CSV listing the same address twice collapses into one contact, leaving imported + updated + skipped below total.

ignored_columns lists any CSV header names that did not match a registered property and were therefore not stored. source_file describes the archived copy of the CSV you uploaded — pass that storage_key back as a storage_key body field (in place of csv) to re-import the same file without uploading it again. contact_limit reports your plan's contact capacity before and after the import, with a human-readable message you can surface directly. When you pass ?segment_id=, an extra segment_added field counts how many of the imported emails were added to that segment.

An empty or invalid CSV returns 422 validation_error. More than 10,000 rows or a body exceeding 5 MB is rejected. An unknown audience returns 404 not_found. See Errors.