---
title: "Create or update contacts in bulk"
method: POST
path: "/v1/contacts/batch"
tags: ["email-contacts"]
---

# Create or update contacts in bulk

`POST /v1/contacts/batch`

## Request

- Base URL: `https://us1.platform.bird.com`
- URL: `https://us1.platform.bird.com/v1/contacts/batch`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `bird_session`

Creates or updates up to 1,000 contacts in one request. Each entry is matched automatically against every identifier it supplies: its email address (trimmed and lowercased), its phone number (normalized to international form), and your own `external_id`. An entry with no match creates a contact. An entry whose identifiers all match one contact updates the supplied fields and preserves omitted fields. This lets an email address change under a stable `external_id` without creating a second contact. An entry whose identifiers belong to several contacts fails with an error naming each match; contacts are never merged automatically. Supplying `match_on` makes that field the only matching key, and every entry must include it. You can also add every contact in the request to up to 10 audiences.

Each entry succeeds or fails on its own: the response lists one result per contact in submission order (`created`, `updated`, or `failed` with the reason), and a failed entry does not abort the rest. If the request itself is invalid, for example when an entry in `audience_ids` does not exist, the whole request fails with a validation error and no contacts are written.

The JSON request body can contain up to 24 MiB (25,165,824 bytes), including escaped characters. Larger requests return HTTP `413` before any contacts are written. Split an oversized request into smaller batches.

## Headers

- `Idempotency-Key` string
- `X-Workspace-Id` string
- `X-Organization-Id` string

## Request body

- ContactUpsertRequest
  - `contacts` ContactBatchEntry[], required: Contacts to create or update, matched automatically against every identifier an entry supplies. Existing contacts are updated with the fields each entry supplies; omitted fields keep their stored values, so an entry can set fields but never clear them. Unmatched entries create contacts.
    - `email` string: Email address, up to 254 characters. Trimmed and lowercased before matching. Invalid addresses fail this contact.
    - `phone_number` string: Phone number with a country code, up to 32 characters. Spaces and punctuation are accepted. An empty string is treated as omitted.
    - `first_name` string: First name, up to 100 characters.
    - `last_name` string: Last name, up to 100 characters.
    - `external_id` string: Your identifier for the contact, up to 254 characters. Unique within the workspace when set.
    - `data` object: Custom contact property values. Keys must be registered contact properties, including archived ones. Values must match their declared type. Strings can contain up to 500 characters and the serialized map is limited to 2 KB. Invalid values fail this contact. Null values remove keys when updating and are ignored when creating.
  - `audience_ids` AudienceID[]: Audiences every contact in this request is added to. Contacts that are already members are left in place. Every listed audience must exist, or the whole request fails with a validation error and nothing is written.
  - `match_on` 'email' | 'phone_number' | 'external_id': A contact identifier a batch entry can be matched on.
  - `data_mode` 'merge' | 'replace': How a supplied `data` object is applied to an existing contact. The default `merge` mode adds the supplied keys to the contact's stored custom values. A key with a `null` value deletes that key. The `replace` mode overwrites the whole stored `data` map with the supplied map. In both modes a contact that omits `data` keeps its stored values unchanged, so an import that touches one attribute never wipes the others.

## Response `200`

Per-contact results, in submission order.

- ContactUpsertResult
  - `data` ContactUpsertResultItem[], required: One entry per contact in the request, in submission order.
    - `entry` ContactUpsertEntry, required: The identifiers a batch entry supplied, in the normalized form used for matching. A field is `null` when the entry did not include it. These values identify the request entry and do not represent the contact's current state.
      - `email` string, nullable, required: Email address this entry carried, trimmed and lowercased. `null` when the entry carried none.
      - `phone_number` string, nullable, required: Phone number this entry carried, in its normalized international form. `null` when the entry carried none. A row rejected for an invalid phone echoes the value as sent, trimmed, since no normalized form exists.
      - `external_id` string, nullable, required: Your own identifier for this entry, when the entry supplied one.
    - `matched_on` 'email' | 'phone_number' | 'external_id' | 'null', nullable, required: Which identifier matched a batch entry to an existing contact. `null` when the entry created a new contact.
    - `status` 'created' | 'updated' | 'failed', required: What happened to this contact. - `created`: a new contact was created for the address. - `updated`: an existing contact with the address was updated. - `failed`: the entry was rejected and `error` explains why. A failed entry does not affect the other entries in the request.
    - `contact_id` string
    - `error` ContactUpsertError
      - `type` string, required: Machine-readable error category for this entry, such as `validation_error` or `conflict_error`, in the same vocabulary as the top-level error `type`. New categories may be added over time, so treat unrecognized values as a generic failure.
      - `code` string, required: Specific error code for this entry, from the same catalog as the top-level error `code`. `E04058` means the entry matched two contacts and requires review. `E04055` means the phone number belongs to another contact and you must retry with different data. Both are `conflict_error` errors; the code distinguishes them.
      - `message` string, required: Human-readable explanation of why this entry failed.
      - `param` string: The field in this entry that caused the failure, such as `data` or `email`. Omitted when the failure names no field.
      - `details` ErrorDetail[]: Per-field problems with this entry, in the same shape as the top-level error `details`. A contact property failure names the property as `data.<key>`, such as `data.plan`. Omitted when the failure has no per-field problems.
        - `param` string, required: Dotted field path, such as `to[0].email`, `subject`, or `.`. When the request was rejected for a query parameter the endpoint does not declare, this carries that parameter's name instead of a field path.
        - `message` string, required: What is wrong with this field.

## Other responses

- `400`: Bad request
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Resource not found
- `413`: Request body or message size exceeds the allowed limit
- `422`: The request has invalid field values, violates a business rule, or carries a query parameter the endpoint does not declare. Field validation errors use `type: validation_error` and include the affected fields in `details`. Business-rule errors identify the failed rule in `type`.
- `429`: Rate limit exceeded
- `500`: Internal server error
- `503`: The service is temporarily unavailable. If `Retry-After` is present, wait for that delay before retrying; otherwise, retry with exponential backoff. Reuse the same idempotency key and request when retrying a mutation.

---

[API](https://skmtc.dev/messagebird/apis/bird-api.md) · [All operations](https://skmtc.dev/messagebird/apis/bird-api/llms.txt) · [OpenAPI document](https://skmtc.dev/messagebird/apis/bird-api/revisions/3e2a50466454?raw)
