---
title: "Create or update contacts"
method: POST
path: "/v1/contacts"
tags: ["Contacts"]
---

# Create or update contacts

`POST /v1/contacts`

Upserts a single contact OR a batch (`{ contacts: [...] }`, up to 1000 rows). Unknown custom fields auto-create field definitions on the brand.

Single: `201` with `{ contact, created, fieldsCreated, warnings }`. Batch: `200` with `{ summary, fieldsCreated, errors, warnings }` — or `207` when some rows failed (per-row errors in `errors[]`).

## Headers

- `Idempotency-Key` string

## Request body

- union
  - ContactsPostSingleRequest
    - `email` string, required
    - `firstName` string
    - `lastName` string
    - `subscribed` boolean
    - `customFields` object
    - `validate` boolean — Optional deliverability check on ingestion. When true, each address is validated with the provider (2 credits per address, charged on success) and the verdict is saved to the contact’s `validationStatus`. Submissions above the inline cap (100 addresses) upsert first and validate as a background job, returning a `validationJobId`.
  - ContactsPostBatchRequest
    - `contacts` object[], required
      - `email` string
      - `firstName` string
      - `lastName` string
      - `subscribed` boolean
      - `customFields` object
    - `validate` boolean — Optional deliverability check on ingestion. When true, each address is validated with the provider (2 credits per address, charged on success) and the verdict is saved to the contact’s `validationStatus`. Submissions above the inline cap (100 addresses) upsert first and validate as a background job, returning a `validationJobId`.

## Response `200`

Batch upsert applied (every row succeeded).

- ContactsPostBatchResponse
  - `summary` object, required
    - `inserted` integer, required
    - `updated` integer, required
    - `failed` integer, required
  - `fieldsCreated` string[], required
  - `errors` object[], required
    - `email` string, required
    - `code` string, required
    - `message` string, required
  - `warnings` object[], required
    - `code` string, required
    - `field` string
    - `message` string, required
    - `from` string
    - `to` string
    - `original` string
    - `normalized` string
  - `validation` object
    - `valid` integer, required
    - `risky` integer, required
    - `invalid` integer, required
    - `unscored` integer, required
  - `validationJobId` string

## Other responses

- `201` — Single upsert applied.
- `207` — Batch partially applied — inspect `errors[]` for each failed row.
- `400` — The request body or query string was invalid (unknown key, wrong type, or missing required field). Strict schemas reject unknown keys — including `brandId`, which is always resolved from the API key.
- `401` — The API key was missing, invalid, or revoked.
- `403` — The caller does not have the required `contacts` permission.
- `409` — The same `Idempotency-Key` was reused with a different request body.
- `422` — Single upsert without an `email`.
- `429` — The request hit the rolling rate limit window.
- `500` — Unexpected internal error.

---

[API](https://skmtc.net/brew/apis/brew-public-api-v1.md) · [All operations](https://skmtc.net/brew/apis/brew-public-api-v1/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/brew/brew-public-api-v1/versions/db96cfedc6c5/schema)
