---
title: "Bulk create contacts"
method: POST
path: "/audience/contacts/bulk"
tags: ["Audience"]
---

# Bulk create contacts

`POST /audience/contacts/bulk`

## Request

- Base URL: `https://app.lettr.com/api`
- URL: `https://app.lettr.com/api/audience/contacts/bulk`
- Auth: HTTP bearer

Create up to 1000 contacts in a single request. Send `contacts` to give each row its own properties, lists and topics, or the legacy `emails` array when the whole batch shares one property map. Emails are normalized and deduplicated; addresses that already exist are reported in `already_existed`, are always attached to the requested lists and topics and always have a row-level `opt_out` applied, and have their properties merged only when `update_existing` is set. Rows that fail validation are skipped and returned in `errors` — the request only fails with 422 when nothing could be written. The response carries the contact ids, so a follow-up call to the bulk list or topic endpoints needs no lookup. Requires the `audience:write` scope and is blocked for sandbox API keys.

## Request body

- union: Bulk create contacts, up to 1000 per request. Send either `emails` (one shared property map for the whole batch) or `contacts` (a row per contact, each with its own properties, lists and topics) — not both. Emails are normalized (lowercased, trimmed) and deduplicated, with the last row for an address winning. Batch-wide `list_ids` and `topics` are unioned into every row.
  - object
    - `emails` string[], required: Legacy shape: a flat list of addresses that all share `properties` and `list_id`.
    - `list_id` string, uuid, nullable: Optional list to add all contacts to. Kept for backwards compatibility; folded into `list_ids`.
    - `properties` object, nullable: Custom property values applied to every contact in the batch. Works with both shapes; with `contacts`, a key set on a row overrides the batch value for that key.
    - `contacts` BulkAudienceContactRow[]: One row per contact. A row that fails validation is skipped and reported in the response `errors` array — the rest of the batch is still written.
      - `email` string, email, required
      - `properties` object, nullable: Custom property values for this contact. Each key must match a property defined for the team.
      - `list_ids` string[], nullable: Lists this contact is added to, on top of the batch-wide ones.
      - `topics` AudienceTopicSubscription[], nullable: Topic subscriptions for this contact.
        - `id` string, uuid, required
        - `subscription` 'opt_in' | 'opt_out': `opt_in` subscribes the contact. `opt_out` suppresses the topic — including topics that auto-subscribe new contacts — and, with `update_existing`, unsubscribes a contact that was already subscribed.
    - `list_ids` string[], nullable: Lists every contact in the batch is added to.
    - `topics` AudienceTopicSubscription[], nullable: Topic subscriptions applied to every contact in the batch. A row-level `opt_out` wins over a batch-level `opt_in` for that contact.
      - `id` string, uuid, required
      - `subscription` 'opt_in' | 'opt_out': `opt_in` subscribes the contact. `opt_out` suppresses the topic — including topics that auto-subscribe new contacts — and, with `update_existing`, unsubscribes a contact that was already subscribed.
    - `update_existing` boolean: When true, contacts that already exist have their properties merged — submitted keys overwrite, others are preserved. When false their properties are left alone and they are counted in `already_existed`. This flag governs property merges only: lists, topic opt-ins and topic `opt_out` entries are always applied, so a row-level `opt_out` drops an existing subscription whether or not the flag is set.
  - object
    - `emails` string[]: Legacy shape: a flat list of addresses that all share `properties` and `list_id`.
    - `list_id` string, uuid, nullable: Optional list to add all contacts to. Kept for backwards compatibility; folded into `list_ids`.
    - `properties` object, nullable: Custom property values applied to every contact in the batch. Works with both shapes; with `contacts`, a key set on a row overrides the batch value for that key.
    - `contacts` BulkAudienceContactRow[], required: One row per contact. A row that fails validation is skipped and reported in the response `errors` array — the rest of the batch is still written.
      - `email` string, email, required
      - `properties` object, nullable: Custom property values for this contact. Each key must match a property defined for the team.
      - `list_ids` string[], nullable: Lists this contact is added to, on top of the batch-wide ones.
      - `topics` AudienceTopicSubscription[], nullable: Topic subscriptions for this contact.
        - `id` string, uuid, required
        - `subscription` 'opt_in' | 'opt_out': `opt_in` subscribes the contact. `opt_out` suppresses the topic — including topics that auto-subscribe new contacts — and, with `update_existing`, unsubscribes a contact that was already subscribed.
    - `list_ids` string[], nullable: Lists every contact in the batch is added to.
    - `topics` AudienceTopicSubscription[], nullable: Topic subscriptions applied to every contact in the batch. A row-level `opt_out` wins over a batch-level `opt_in` for that contact.
      - `id` string, uuid, required
      - `subscription` 'opt_in' | 'opt_out': `opt_in` subscribes the contact. `opt_out` suppresses the topic — including topics that auto-subscribe new contacts — and, with `update_existing`, unsubscribes a contact that was already subscribed.
    - `update_existing` boolean: When true, contacts that already exist have their properties merged — submitted keys overwrite, others are preserved. When false their properties are left alone and they are counted in `already_existed`. This flag governs property merges only: lists, topic opt-ins and topic `opt_out` entries are always applied, so a row-level `opt_out` drops an existing subscription whether or not the flag is set.

## Response `201`

Contacts created

- BulkStoreAudienceContactsResponse
  - `message` string, required
  - `data` object, required
    - `created` integer, required
    - `already_existed` integer, required: Submitted addresses that were already in the audience.
    - `updated` integer, required: Existing contacts the request changed — properties merged, a list or topic attached, or a subscription dropped.
    - `error_count` integer, required
    - `errors` BulkAudienceContactError[], required: Rows that were skipped. Empty when the whole batch was accepted.
      - `index` integer, required: Zero-based position of the row in the request.
      - `email` string, nullable, required
      - `error_code` 'missing_email' | 'invalid_email' | 'invalid_property_value' | 'unknown_property_key' | 'unknown_list' | 'unknown_topic' | 'invalid_topic_subscription', required
      - `error` string, required
    - `contacts` BulkAudienceContactRef[], required: Every contact in the batch that exists after the request, in submission order.
      - `id` string, uuid, required
      - `email` string, email, required
      - `created` boolean, required: True when this request created the contact.

## Other responses

- `401`: Unauthorized - Missing or invalid API key
- `403`: Forbidden - API key does not have the required scope
- `422`: Validation error
- `500`: Internal server error
- `503`: Service unavailable - the API key could not be verified, so the request was not processed. This does not mean the key is invalid; retry after the delay given in the Retry-After header.

---

[API](https://skmtc.dev/lettr/apis/lettr-api.md) · [All operations](https://skmtc.dev/lettr/apis/lettr-api/llms.txt) · [OpenAPI document](https://skmtc.dev/lettr/apis/lettr-api/revisions/48ef0dac1ebd?raw)
