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

# Batch operations

> Batch contact upsert + delete on the Brew Public API v1 — payload limits, partial-success contract, per-row error reporting.

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

| Endpoint              | Batch shape           | Max rows | Rate-limit policy                 |
| --------------------- | --------------------- | -------- | --------------------------------- |
| `POST /v1/contacts`   | `{ contacts: [...] }` | 1000     | `contacts.write_batch` (10 / min) |
| `DELETE /v1/contacts` | `{ emails: [...] }`   | 1000     | `contacts.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.

```json theme={null}
{
  "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." }
  ]
}
```

| Field              | Meaning                                                                                                               |
| ------------------ | --------------------------------------------------------------------------------------------------------------------- |
| `summary.inserted` | Rows that didn't exist before this request.                                                                           |
| `summary.updated`  | Rows that existed and were updated.                                                                                   |
| `summary.failed`   | Rows in `errors[]`. The total of `inserted + updated + failed` matches the input length.                              |
| `fieldsCreated`    | Custom 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](/api-reference/api/pagination) for reading them back.

```bash theme={null}
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`):

```json theme={null}
{
  "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

```ts theme={null}
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.

```bash theme={null}
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`):

```json theme={null}
{
  "deleted": 1,
  "notFound": ["missing@example.com"]
}
```

## Common per-row error codes

| Code                  | When                                                                                                                      |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `MISSING_EMAIL`       | Row in `contacts[]` omitted `email`.                                                                                      |
| `INVALID_EMAIL`       | `email` is not RFC-5322 compliant.                                                                                        |
| `DUPLICATE_EMAIL`     | Two rows in the same batch had the same email (only the last write survives). Surfaced as a warning, not an error.        |
| `FIELD_TYPE_MISMATCH` | A 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](/api-reference/api/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 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](/api-reference/api/idempotency) — set `Idempotency-Key` on the batch so a retry doesn't replay 1000 rows.
* [Rate limits](/api-reference/api/rate-limits) — `contacts.write_batch` is 10/min (vs single at 100/min); plan accordingly.
* [Pagination](/api-reference/api/pagination) — for reading back the imported rows.
* [Errors](/api-reference/api/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:

<Tabs>
  <Tab title="Self-Service Tools">
    <CardGroup cols="2">
      <Card title="Search Documentation" icon="magnifying-glass" color="#c44925">
        Type in the "Ask any question" search bar at the top left to instantly find relevant documentation pages.
      </Card>

      <Card title="ChatGPT/Claude Integration" icon="robot" color="#c44925">
        Click "Open in ChatGPT" at the top right of any page to analyze documentation with ChatGPT or Claude for deeper insights.
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="Talk to Our Team">
    <CardGroup cols="2">
      <Card title="Schedule a Call" icon="calendar" color="#c44925" href="https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ1iYoRUG1J792XQpbuQLjSRRDupr7MwraFK-HQRCtTYdBmrQi8nZu2qXfzKQigb8gbKJK3KN3-R">
        Book time with our founders for personalized guidance on strategy, best practices, or complex implementation questions.
      </Card>

      <Card title="Call Us Directly" icon="phone" color="#c44925">
        Need immediate assistance? Reach us at **+1-(332)-203-2145** for urgent issues or time-sensitive questions.
      </Card>

      <Card title="Slack Channel" icon="slack" color="#c44925">
        Our preferred support channel. You'll receive an invite after signup for direct founder support and fast responses.
      </Card>

      <Card title="Email Support" icon="envelope" color="#c44925" href="mailto:support@brew.new">
        Contact us at **[support@brew.new](mailto:support@brew.new)** for detailed inquiries or if you prefer not to use Slack.
      </Card>
    </CardGroup>
  </Tab>
</Tabs>
