Skip to main content
Brew accepts batch writes on the contacts routes so agents and back-end importers can upsert / delete many rows in a single request without burning the per-request rate-limit budget. Batches are partial-success-safe: one bad row doesn’t fail the whole call.

Where It Applies

Single-row upserts use POST /v1/contacts with a different body shape and the higher-rate contacts.write_single policy (100 / min). Single-row deletes use DELETE /v1/contacts/{email}. Choose the shape that matches your workload: 100/min × 1 row = 100 contacts/min; 10/min × 1000 rows = 10,000 contacts/min.

Partial-Success Contract

When any rows fail, including when every row fails, the batch upsert returns 207 instead of 200. Failures are surfaced in errors[] so callers can fix and re-submit the rejected rows.
For a completed JSON batch upsert response, inserted + updated + failed equals the number of input rows. Two duplicate rows contribute failed: 2. Use summary.failed to count rejected rows.

POST /v1/contacts: Batch Upsert

Up to 1000 rows per request. email is the merge key; missing or malformed emails land in errors[]. Custom fields auto-create field definitions on the brand on first sight. See Pagination for reading them back. The addresses below are synthetic fixtures. Replace them with test contacts you control.
Assume John exists, Jane is new, and the brand has no plan field definition. The response is 207 because bad@ fails validation:

TypeScript SDK

POST /v1/contacts/batch-delete: Batch Delete

Up to 1000 emails per request. The endpoint is idempotent: deleting a non-existent email surfaces in notFound[], not as an error.
Assuming Jane exists and missing@brewmail.dev does not, the response is 200:

Common Per-Row Error Codes

Email matching trims surrounding whitespace and ignores case. Deduplicate those normalized addresses before retrying. To unsubscribe a contact, submit one unambiguous row with the address and top-level subscribed: false to POST /v1/contacts. A rejected duplicate group applies no subscription changes. Foundational codes from Errors (for example RATE_LIMITED, INVALID_REQUEST) only ever apply to the whole request, never to individual rows.

Auto-Created Field Definitions

When a batch upsert references a custom field the brand doesn’t have yet, Brew creates the field definition before the write. The new column shows up immediately in:
  • the contacts page (filterable / sortable / searchable),
  • the audience builder,
  • GET /v1/fields,
  • POST /v1/contacts/search with { "logic": "and", "filters": [{ "field": "plan", "operator": "equals", "value": "pro" }] }.
Field names are normalized, so snake_case, kebab-case, and spaces all converge on the same camelCase field. Normalization events surface in warnings[] as FIELD_NAME_NORMALIZED.

Partial Success Design

Batch writes report aggregate counts and failed rows so invalid data does not block valid contacts. The contract and response shape are above; see API design notes for the rationale. If your workflow requires all-or-nothing, validate and stage the input before calling the endpoint. Do not infer which successful rows were newly inserted from aggregate counts and delete them, because a batch can also update existing contacts.
  • Idempotency: set Idempotency-Key on the batch so a retry doesn’t replay 1000 rows.
  • Rate limits: contacts.write_batch is 10/min (vs single at 100/min); plan accordingly.
  • Pagination: for reading back the imported rows.
  • Errors: full error catalog for the foundational request-level codes.

Need Help?

Our team is ready to support you at every step of your journey with Brew. Choose the option that works best for you:

Search Documentation

Type in the “Ask any question” search bar at the top left to instantly find relevant documentation pages.

Ask ChatGPT or Claude

Click here to open the Copy page menu at the top of this page, then send the page to ChatGPT, Claude, or your editor.