Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/DataTalksClub/datamailer/llms.txt

Use this file to discover all available pages before exploring further.

POST /api/contacts/imports accepts a JSON array of contact objects and upserts each one using the same logic as the single-contact POST /api/contacts endpoint. It is designed for bulk synchronization from external systems: invalid rows are reported in the response while valid rows are still committed, and re-sending the same records is safe because operations are idempotent by normalized email plus audience/client scope.

Authentication

All requests must include a Bearer token issued for the target client.
Authorization: Bearer <client-api-key>

Request

Method and path: POST /api/contacts/imports Content-Type: application/json

Body Parameters

contacts
object[]
required
Array of contact objects to import. Each object supports the same fields as the upsert endpoint. audience and client can be provided at the top level of the request body as defaults and will be merged into any item that omits them.
audience
string
Default audience slug applied to all items that do not include their own audience field.
client
string
Default client slug applied to all items that do not include their own client field.
dry_run
boolean
If true, validates all rows and returns projected actions (would_create, would_update) without writing any data to the database. Defaults to false.
idempotency_key
string
Optional string stored alongside the import result for your own reference. Not used for deduplication by Datamailer.

Idempotency

Operations are idempotent by normalized email combined with the audience/client scope. Re-importing the same contact record produces an unchanged action and does not overwrite timestamps. Within a single request, the first occurrence of a duplicate email+scope key is processed and later duplicates are skipped with reason: duplicate_input.

Partial Errors

Invalid rows are collected and returned in the errors array without stopping the import. All valid rows in the same request are committed regardless of which other rows fail.

Response

HTTP 200 OK. The response contains overall counts, per-item results, and per-item errors.
dry_run
boolean
Echoes the dry_run flag from the request.
idempotency_key
string
Echoes the idempotency_key from the request, or empty string.
counts
object
results
object[]
Per-item result entries for all processed (non-error) rows, including skipped duplicates.
errors
object[]
Per-item validation error entries for rows that could not be imported.

Example

Request

curl -X POST https://datamailer.example.com/api/contacts/imports \
  -H "Authorization: Bearer dm_dtcnews_demo_newsletter_import_export_key" \
  -H "Content-Type: application/json" \
  -d '{
    "audience": "dtc-courses",
    "client": "dtc-courses",
    "contacts": [
      {
        "email": "learner@example.com",
        "status": "subscribed",
        "tags": ["course-ml-zoomcamp"],
        "verified": true
      },
      {
        "email": "student@example.com",
        "status": "subscribed",
        "tags": ["course-de-zoomcamp"],
        "email_validation": {
          "status": "valid",
          "reason": "smtp-check"
        }
      },
      {
        "email": "not-an-email",
        "status": "subscribed"
      }
    ]
  }'

Response

{
  "dry_run": false,
  "idempotency_key": "",
  "counts": {
    "total": 3,
    "created": 1,
    "updated": 1,
    "unchanged": 0,
    "skipped": 0,
    "invalid": 1
  },
  "results": [
    {
      "index": 0,
      "item": 1,
      "email": "learner@example.com",
      "action": "unchanged",
      "contact": { "contact_id": 42, "email": "learner@example.com" }
    },
    {
      "index": 1,
      "item": 2,
      "email": "student@example.com",
      "action": "created",
      "contact": { "contact_id": 43, "email": "student@example.com" }
    }
  ],
  "errors": [
    {
      "index": 2,
      "item": 3,
      "email": "",
      "errors": { "email": "invalid" }
    }
  ]
}
For large datasets, send contacts in batches of 100 rows or fewer per request. Each batch is processed in a single database transaction per row, so smaller batches reduce lock contention and make partial-error recovery easier to manage on your end.

Build docs developers (and LLMs) love