Skip to main content
Brew accepts batch writes on /v1/contacts 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 writes use the same routes with a different body shape and the higher-rate contacts.write_single policy (100 / min). 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

The HTTP status is 200 even when some rows fail. Per-row failures are surfaced in errors[] so callers can iterate, fix, and re-submit just the bad 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.
Response (200):

TypeScript SDK

DELETE /v1/contacts: Batch Delete

Up to 1000 emails per request. The endpoint is idempotent: deleting a non-existent email surfaces in notFound[], not as an error.
Response (200):

Common Per-Row Error Codes

Foundational codes from Errors (e.g. 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 column that doesn’t have a fieldDefinitions row on the brand yet, the helper auto-creates one 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 via normalizeAudienceFieldName so snake_case + kebab-case + spaces all converge on the same camelCase column. Normalization events surface in warnings[] as FIELD_NAME_NORMALIZED.

Why Partial Success, Not “All-or-Nothing”

A 1000-row import where 3 rows have bad emails should not block the 997 valid rows. Brew’s contract:
  1. Validate every row independently.
  2. Write the valid ones in a single bulk Mongo operation.
  3. Report failed rows in errors[] keyed on email.
  4. Return HTTP 200 with the summary.
To enforce all-or-nothing on the caller side, check summary.failed === 0 and roll back yourself (e.g. by DELETE on the inserted emails).

See Also

  • 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.

ChatGPT/Claude Integration

Click “Open in ChatGPT” at the top right of any page to analyze documentation with ChatGPT or Claude for deeper insights.