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

# Migrate to the New v1 Surface

> Every route, field, status, and error rename in the Brew Public API v1 cleanup, with the old spelling next to the new one so you can grep your code.

The v1 cleanup gave every collection a real detail read, moved sends to their
own root, turned lifecycle changes into action sub-paths, and collapsed four
overlapping status vocabularies into one. There are no aliases and no
deprecation window: the old spellings return `400` or `404` today.

This page is the grep list. Each row is the old string to search your codebase
for and the string to replace it with. The generated
[Public API v1 reference](/api-reference/public-v1/contacts/get-contacts) is
the authority on the exact request and response shapes.

## 1. Read One Row by Its Path

The old trick was to call a list with an id filter and take `data[0]`. Those
query keys are gone and now return `400 INVALID_REQUEST` with `param` naming
the key. Every detail read returns the **bare row**, so drop the `data[0]`
unwrap too.

| Old                                                                     | New                                                               |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `GET /v1/emails?emailId=`                                               | `GET /v1/emails/{emailId}`                                        |
| `GET /v1/audiences?audienceId=`                                         | `GET /v1/audiences/{audienceId}`                                  |
| `GET /v1/automations?automationId=`                                     | `GET /v1/automations/{automationId}`                              |
| `GET /v1/automations/runs?automationRunId=`                             | `GET /v1/automations/runs/{automationRunId}`                      |
| `GET /v1/automations/triggers?triggerEventId=`                          | `GET /v1/automations/triggers/{triggerEventId}`                   |
| `GET /v1/domains?domainId=`                                             | `GET /v1/domains/{domainId}`                                      |
| `GET /v1/email-groups?groupId=`                                         | `GET /v1/email-groups/{groupId}`                                  |
| `GET /v1/fields?fieldName=`                                             | `GET /v1/fields/{fieldName}`                                      |
| A `POST /v1/contacts/search` filter on one address                      | `GET /v1/contacts/{email}`                                        |
| `GET /v1/flows?slug=` → `{ data: [flow] }`, `?include=html` on the list | `GET /v1/flows/{slug}` → the bare flow; `include=html` there only |

New reads with no predecessor: `GET /v1/automations/audience-runs/{audienceRunId}`,
`GET /v1/automations/trigger-instances/{triggerInstanceId}`, and
`GET /v1/emails/{emailId}/inbox-placement-tests/{testId}`.

The `?include=` opt-ins still work, now on the path read:
`GET /v1/emails/{emailId}?include=html,versions`. The full list is in
[Pagination](/api-reference/api/pagination).

<Note>
  An id in a query string is not always wrong. `GET /v1/sends` and
  `GET /v1/automations/runs` still take `automationId`, `automationRunId`,
  `audienceRunId`, and `triggerInstanceId` as deliberate **join** filters,
  because those return many rows, not one.
</Note>

## 2. Sends Moved to Their Own Root

| Old                                              | New                                                     |
| ------------------------------------------------ | ------------------------------------------------------- |
| `GET /v1/analytics/sends`                        | `GET /v1/sends`                                         |
| `GET /v1/analytics/sends?sendId=`                | `GET /v1/sends/{sendId}`                                |
| `GET /v1/analytics/sends?sendId=&include=events` | `GET /v1/sends/{sendId}?include=events`                 |
| `GET /v1/analytics/campaigns`                    | `GET /v1/sends?kind=campaign`; each row carries `stats` |
| `GET /v1/analytics/trigger-instances`            | `GET /v1/automations/trigger-instances`                 |

`/v1/analytics` keeps exactly three reports: `overview`, `automations`, and
the unified `events` feed. See
[Monitor campaign sends](/api-reference/guides/monitor-campaign-sends).

## 3. Lifecycle Changes Are Action Sub-Paths

A status is no longer something you PATCH onto a row.

| Old                                                                     | New                                                       |
| ----------------------------------------------------------------------- | --------------------------------------------------------- |
| `PATCH /v1/domains { domainId, verify: true }`                          | `POST /v1/domains/{domainId}/verify`                      |
| `PATCH /v1/automations/runs { automationRunId, status: 'canceled' }`    | `POST /v1/automations/runs/{automationRunId}/cancel`      |
| `POST /v1/automations/audience-runs/{audienceRunId}/control { action }` | `POST …/{audienceRunId}/pause`, `/resume`, `/cancel`      |
| `GET /v1/automations/triggers/{triggerEventId}/fire` as a ready check   | `GET /v1/automations/triggers/{triggerEventId}/readiness` |

Cancel answers `200` with the new `status` and, for an automation run, the
`previousStatus` it left. The readiness body is bare:
`{ ready, blockers[], payloadSchema, endpoint, publishedAutomations, counts }`,
with no `details` wrapper.

## 4. Renamed Request and Response Fields

| Old                                                               | New                                                                                              | Where                                                                                                     |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `fromEmail` + `senderName`                                        | `from: { email, name? }`                                                                         | `POST /v1/sends`                                                                                          |
| `replyTo` nested under the sender                                 | `replyTo` at the top level                                                                       | `POST /v1/sends`                                                                                          |
| `runId` on the send `202` and on send rows                        | Gone. `sendId` is the only handle.                                                               | `POST /v1/sends`, `GET /v1/sends`                                                                         |
| `{ version }`                                                     | `{ emailVersionId }`                                                                             | `POST /v1/emails/{emailId}/restore`                                                                       |
| `createdAtFrom` / `createdAtTo` / `updatedAtFrom` / `updatedAtTo` | `sortBy=createdAt\|updatedAt` plus `from` / `to`, which bound whichever timestamp `sortBy` names | `GET /v1/emails`                                                                                          |
| `recipientEmail` as a filter                                      | `recipient`, which also accepts `@domain`, a substring, and `!` exclusions                       | `GET /v1/analytics/events`                                                                                |
| `dry_run`                                                         | `dryRun`, in the request and in the preview response                                             | `POST /v1/automations`, `PATCH /v1/automations/{automationId}`, `POST /v1/automations/{automationId}/run` |
| `stop_in_flight`                                                  | `stopInFlight`                                                                                   | `PATCH /v1/automations/{automationId}`                                                                    |

Those two were the last snake\_case fields on the wire. Everything v1 accepts
and returns is camelCase now.

Send rows gained `automationId`, `nodeId`, `automationRunId`, `audienceRunId`,
and `triggerInstanceId`, so an automation delivery can be joined back to the
run that produced it without a second call.

Two responses became explicit rather than a bare count:
`POST /v1/contacts/batch-delete` returns `{ deletedCount, notFound[] }`, and
`DELETE /v1/api-keys/{keyId}` returns `{ keyId, deleted, revokedAt? }`.

<Note>
  The event **row** field is still `recipientEmail`. Only the query filter was
  renamed to `recipient`.
</Note>

### `TriggerInstance.state` Became an Enum

A fired trigger's `state` used to be a free string. It is now a closed set you
can branch on:

```
received | verified | matched | partially_fired | fired | rejected | dead_letter
```

`fired` means every matched automation started. `partially_fired` means some
starts are still being retried, so it is not a terminal answer: read the
instance again rather than treating it as a failure.

## 5. One Status Vocabulary

Runs, sends, audience builds, and inbox-placement tests all report the same
set. Anything typed against an old spelling in a status **filter** is now
`400 INVALID_REQUEST`.

```
queued | scheduled | running | paused | completed | partially_completed | failed | canceled
```

| Old              | New                              |
| ---------------- | -------------------------------- |
| `sent`           | `completed`                      |
| `partially_sent` | `partially_completed`            |
| `sending`        | `running`                        |
| `pending`        | `queued`                         |
| `cancelled`      | `canceled` (one `l`, everywhere) |

A step or node inside a run reports `running | completed | failed | skipped`.
An email **design** reports `generating | ready | failed`, so the old
`streaming` and `complete` spellings are gone there too. A synchronous test
send now resolves to `200 { status: 'completed', recipient }`.

## 6. Errors

| Old                                                                      | New                                                                                          |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| `INSUFFICIENT_EMAIL_SENDS`                                               | `SEND_QUOTA_EXCEEDED`, `402`                                                                 |
| `PUBLISH_VALIDATION_FAILED` at `409`                                     | Same code at `422`                                                                           |
| `EVENT_NOT_FOUND`                                                        | `TRIGGER_INSTANCE_NOT_FOUND`                                                                 |
| Any other `429` code                                                     | `RATE_LIMITED` is the only one                                                               |
| The trigger-fire "legacy envelope" (`{ success, details }`)              | The standard error envelope; a bad payload is `400 INVALID_PAYLOAD`                          |
| A generic `404 NOT_FOUND` from `POST /v1/automations/{automationId}/run` | `404 AUTOMATION_NOT_FOUND` or `404 AUDIENCE_NOT_FOUND`, so you can tell which one is missing |

New this cleanup: `403 ORG_SCOPE_REQUIRED` when a brand-bound key calls an
operation that acts on the organization, such as `GET /v1/usage`. A person
missing the role still gets `403 INSUFFICIENT_ROLE`. See
[Organization and brand credentials](/api-reference/api/organization-and-brand-scope).

The full catalog, generated from the specification, is on
[Errors](/api-reference/api/errors).

## 7. TypeScript SDK 10

Twenty methods were added, mostly the detail reads and the send root:

| Method                                                   | Route                                                       |
| -------------------------------------------------------- | ----------------------------------------------------------- |
| `audiences.get(audienceId, { include? })`                | `GET /v1/audiences/{audienceId}`                            |
| `automations.get(automationId, { include? })`            | `GET /v1/automations/{automationId}`                        |
| `automations.runs.get(automationRunId, { include? })`    | `GET /v1/automations/runs/{automationRunId}`                |
| `automations.runs.cancel(automationRunId)`               | `POST /v1/automations/runs/{automationRunId}/cancel`        |
| `automations.audienceRuns.get(audienceRunId)`            | `GET /v1/automations/audience-runs/{audienceRunId}`         |
| `automations.audienceRuns.pause(audienceRunId)`          | `POST …/audience-runs/{audienceRunId}/pause`                |
| `automations.audienceRuns.resume(audienceRunId)`         | `POST …/audience-runs/{audienceRunId}/resume`               |
| `automations.audienceRuns.cancel(audienceRunId)`         | `POST …/audience-runs/{audienceRunId}/cancel`               |
| `automations.triggerInstances.list(query)`               | `GET /v1/automations/trigger-instances`                     |
| `automations.triggerInstances.get(triggerInstanceId)`    | `GET /v1/automations/trigger-instances/{triggerInstanceId}` |
| `automations.triggers.get(triggerEventId, { include? })` | `GET /v1/automations/triggers/{triggerEventId}`             |
| `automations.triggers.readiness(triggerEventId)`         | `GET /v1/automations/triggers/{triggerEventId}/readiness`   |
| `contacts.get(email)`                                    | `GET /v1/contacts/{email}`                                  |
| `contacts.list(query)`                                   | `GET /v1/contacts`                                          |
| `domains.get(domainId)`                                  | `GET /v1/domains/{domainId}`                                |
| `emailGroups.get(groupId)`                               | `GET /v1/email-groups/{groupId}`                            |
| `emails.get(emailId, { include? })`                      | `GET /v1/emails/{emailId}`                                  |
| `emails.inboxPlacementTests.get(emailId, testId)`        | `GET /v1/emails/{emailId}/inbox-placement-tests/{testId}`   |
| `fields.get(fieldName)`                                  | `GET /v1/fields/{fieldName}`                                |
| `sends.list(query)`                                      | `GET /v1/sends`                                             |
| `sends.get(sendId, { include? })`                        | `GET /v1/sends/{sendId}`                                    |

And these were removed or retargeted:

| Old                                                    | New                                                                |
| ------------------------------------------------------ | ------------------------------------------------------------------ |
| `analytics.campaigns.*`                                | `sends.list({ kind: 'campaign' })`; rows carry `stats`             |
| `analytics.sends.*`                                    | `sends.list(query)` and `sends.get(sendId, { include: 'events' })` |
| `analytics.triggerInstances.*`                         | `automations.triggerInstances.*`                                   |
| `automations.triggers.ready(…)`                        | `automations.triggers.readiness(…)`, bare body                     |
| `automations.runs.cancel({ automationRunId, status })` | `automations.runs.cancel(automationRunId)`                         |
| `automations.audienceRuns.control({ action })`         | `pause`, `resume`, `cancel` as three methods                       |

`analytics` keeps only the reports: `overview`, `events`, `automations`.

## 8. CLI 0.7

`brew-cli` 0.7 depends on `@brew.new/sdk` 10. Six commands were retargeted at
the new routes, and the command names you already type are unchanged, so
`brew-cli automations triggers ready` still works. New commands cover the
detail reads, the sends root, trigger instances, and the four lifecycle
actions. Raw `brew-cli api` calls in your scripts are **not** retargeted for
you: they go straight to the path you typed, so grep those against section 1.

## Migration Checklist

<Steps>
  <Step title="Grep for the retired query keys">
    Search for `?emailId=`, `?audienceId=`, `?automationId=`, `?automationRunId=`,
    `?triggerEventId=`, `?sendId=`, `?triggerInstanceId=`, and `?domainId=`.
    Each one that reads a single row becomes a path read.
  </Step>

  <Step title="Grep for the analytics paths">
    `analytics/sends`, `analytics/campaigns`, and `analytics/trigger-instances`
    have no replacement at the same path.
  </Step>

  <Step title="Grep for old status strings">
    `'sent'`, `'sending'`, `'partially_sent'`, `'pending'`, `'streaming'`,
    `'complete'`, and `'cancelled'` in filters and comparisons.
  </Step>

  <Step title="Drop the data[0] unwrap">
    A detail read returns the row. `body.data[0]` will be `undefined`.
  </Step>

  <Step title="Drop runId from send handling">
    `POST /v1/sends` no longer returns one. Persist `sendId` instead.
  </Step>

  <Step title="Grep for snake_case request keys">
    `dry_run` and `stop_in_flight` are rejected. Send `dryRun` and
    `stopInFlight`.
  </Step>

  <Step title="Re-check your credential">
    If any call now returns `403 ORG_SCOPE_REQUIRED`, mint an
    organization-scoped key for it. See
    [Organization and brand credentials](/api-reference/api/organization-and-brand-scope).
  </Step>
</Steps>

## See Also

* [Pagination](/api-reference/api/pagination): the detail-read table and the cursor loop.
* [Errors](/api-reference/api/errors): the generated code catalog.
* [Async jobs & polling](/api-reference/api/async-jobs): the poll loops, rewritten for the path reads.
* [Changelog](/changelog/api): the dated record of API changes.

## 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 explore it further with ChatGPT or Claude.
      </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>
