Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.brew.new/llms.txt

Use this file to discover all available pages before exploring further.

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

EndpointBatch shapeMax rowsRate-limit policy
POST /v1/contacts{ contacts: [...] }1000contacts.write_batch (10 / min)
DELETE /v1/contacts{ emails: [...] }1000contacts.write_batch (10 / min)
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.
{
  "summary": { "inserted": 8, "updated": 2, "failed": 1 },
  "fieldsCreated": ["plan", "creditBalance"],
  "errors": [
    { "email": "bad@", "code": "INVALID_EMAIL", "message": "Email is not RFC-5322 compliant." }
  ],
  "warnings": [
    { "code": "FIELD_NAME_NORMALIZED", "field": "first_name", "from": "first_name", "to": "firstName", "message": "Normalised custom-field name." }
  ]
}
FieldMeaning
summary.insertedRows that didn’t exist before this request.
summary.updatedRows that existed and were updated.
summary.failedRows in errors[]. The total of inserted + updated + failed matches the input length.
fieldsCreatedCustom field definitions auto-created on the brand by this request (snake_case / kebab-case → camelCase normalised).
errors[]Per-row failures keyed on email. Each carries a stable code (e.g. MISSING_EMAIL, INVALID_EMAIL).
warnings[]Non-fatal observations (field normalisation, coercion, duplicate emails collapsed).

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.
curl -X POST https://brew.new/api/v1/contacts \
  -H "Authorization: Bearer $BREW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: contacts-import-2026-04-08" \
  -d '{
    "contacts": [
      { "email": "jane@example.com", "firstName": "Jane",  "customFields": { "plan": "pro" } },
      { "email": "john@example.com", "firstName": "John",  "customFields": { "plan": "free" } },
      { "email": "bad@",              "firstName": "Bad" }
    ]
  }'
Response (200):
{
  "summary": { "inserted": 1, "updated": 1, "failed": 1 },
  "fieldsCreated": ["plan"],
  "errors": [
    { "email": "bad@", "code": "INVALID_EMAIL", "message": "Email is not RFC-5322 compliant." }
  ],
  "warnings": []
}

TypeScript SDK

const result = await brew.contacts.upsert({
  contacts: [
    { email: 'jane@example.com', firstName: 'Jane', customFields: { plan: 'pro' } },
    { email: 'john@example.com', firstName: 'John', customFields: { plan: 'free' } },
  ],
})

console.log(result.summary)         // { inserted, updated, failed }
console.log(result.fieldsCreated)   // ['plan'] on first sight
for (const err of result.errors) {
  console.error(`row ${err.email} failed: ${err.code}${err.message}`)
}

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.
curl -X DELETE https://brew.new/api/v1/contacts \
  -H "Authorization: Bearer $BREW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": [
      "jane@example.com",
      "missing@example.com"
    ]
  }'
Response (200):
{
  "deleted": 1,
  "notFound": ["missing@example.com"]
}

Common per-row error codes

CodeWhen
MISSING_EMAILRow in contacts[] omitted email.
INVALID_EMAILemail is not RFC-5322 compliant.
DUPLICATE_EMAILTwo rows in the same batch had the same email (only the last write survives). Surfaced as a warning, not an error.
FIELD_TYPE_MISMATCHA custom-field value didn’t match the existing field definition’s type. Re-emit with a coerced value OR delete the field.
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,
  • GET /v1/contacts?logic=and&filters=[{"field":"plan","operator":"eq","value":"pro"}].
Field names are normalised via normalizeAudienceFieldName so snake_case + kebab-case + spaces all converge on the same camelCase column. Normalisation 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 limitscontacts.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.