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

# Errors

> Standard error envelope, the full Brew Public API v1 error code catalog, and recovery guidance for every code.

Every non-2xx response from the Brew Public API v1 returns the **same JSON envelope**. Branch on `error.code` (stable) — not on `error.message` (human-readable, can change).

## The error envelope

```json theme={null}
{
  "error": {
    "code": "AUTOMATION_GRAPH_INVALID",
    "type": "invalid_request",
    "message": "\"Welcome\" references emailVersionId 'emv_xxx' which does not exist in this brand. (and 1 more graph issue)",
    "param": "nodes[0].config.emailVersionId",
    "suggestion": "Fix every issue reported in `details.issues` then resubmit.",
    "docs": "https://docs.brew.new/api-reference/api/errors",
    "retryAfter": 12,
    "details": { "issues": [/* ... */] }
  }
}
```

| Field        | Type    | When set                                                                                                                                                     |
| ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `code`       | string  | Always. Stable identifier — branch SDK logic on this.                                                                                                        |
| `type`       | enum    | Always. One of `authentication_error \| authorization_error \| invalid_request \| not_found \| conflict \| rate_limit \| not_implemented \| internal_error`. |
| `message`    | string  | Always. Human-readable summary. Can change.                                                                                                                  |
| `param`      | string? | When the failing field is known (e.g. `payloadSchema.fields`, `nodes[0].config.emailVersionId`).                                                             |
| `suggestion` | string  | Always. Concrete recovery hint.                                                                                                                              |
| `docs`       | string  | Always. Deep-link to canonical reference.                                                                                                                    |
| `retryAfter` | int?    | On `429`. Mirrors the `Retry-After` header (seconds).                                                                                                        |
| `details`    | object? | Machine-readable extras (e.g. `details.blockers[]`, `details.issues[]`).                                                                                     |

### `details` shape — `PUBLISH_VALIDATION_FAILED` (409)

When `PATCH /v1/automations/{automationId}` with `{ published: true }` is blocked, `details.blockers[]` enumerates every node-level reason so callers can render a fix-it list.

```json theme={null}
{
  "error": {
    "code": "PUBLISH_VALIDATION_FAILED",
    "type": "conflict",
    "message": "Add at least one Send Email action",
    "details": {
      "blockers": [
        { "nodeId": "send_1", "nodeLabel": "Welcome", "severity": "error", "message": "\"Welcome\" is missing an inbox preview" }
      ]
    }
  }
}
```

### `details` shape — `AUTOMATION_GRAPH_INVALID` (400)

When `POST /v1/automations` (or `PATCH` with new `nodes`/`connections`) fails the server-side FK + structural resolver, `details.issues[]` enumerates every problem. Each carries a `kind` you can branch on:

```json theme={null}
{
  "error": {
    "code": "AUTOMATION_GRAPH_INVALID",
    "type": "invalid_request",
    "details": {
      "issues": [
        { "kind": "email_wrong_type", "nodeId": "send_welcome", "message": "Referenced email is campaign — must be automation or transactional." },
        { "kind": "domain_not_ready", "nodeId": "send_welcome", "message": "Domain not verified for sending." }
      ]
    }
  }
}
```

| `kind`                       | Cause                                                                     |
| ---------------------------- | ------------------------------------------------------------------------- |
| `duplicate_node_id`          | Two nodes share the same `id`.                                            |
| `connection_unknown_from`    | `connection.from` points to a non-existent node.                          |
| `connection_unknown_to`      | `connection.to` points to a non-existent node.                            |
| `connection_targets_trigger` | A connection targets the trigger node (triggers are entry-only).          |
| `connection_self_loop`       | `connection.from === connection.to`.                                      |
| `email_not_found`            | `emailVersionId` doesn't exist in this brand.                             |
| `email_version_mismatch`     | `emailVersionId` exists but belongs to a different `emailId`.             |
| `email_wrong_type`           | Referenced email is `campaign` — must be `automation` or `transactional`. |
| `domain_not_found`           | `domainId` doesn't exist in this brand.                                   |
| `domain_not_ready`           | Domain not verified for sending.                                          |

## Foundational error codes (every endpoint)

These can appear on any v1 endpoint:

| Code                       | HTTP | `type`                 | Recovery                                                                                                                                                        |
| -------------------------- | ---- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTHENTICATION_REQUIRED`  | 401  | `authentication_error` | Set the `Authorization: Bearer <key>` (or `X-API-Key: <key>`) header.                                                                                           |
| `INVALID_API_KEY`          | 401  | `authentication_error` | The key is malformed or unknown. Re-issue at [brew.new/settings/api](https://brew.new/settings/api).                                                            |
| `API_KEY_REVOKED`          | 401  | `authentication_error` | The key was revoked. Re-issue.                                                                                                                                  |
| `INSUFFICIENT_PERMISSIONS` | 403  | `authorization_error`  | The key lacks the route's permission scope. `error.param` carries the missing scope name.                                                                       |
| `METHOD_NOT_ALLOWED`       | 405  | `invalid_request`      | Wrong verb on this resource.                                                                                                                                    |
| `INVALID_REQUEST`          | 400  | `invalid_request`      | Zod failure / unknown body key / malformed JSON / sending a removed field (e.g. `provider` on `POST /v1/automations/triggers`). Fix the body per `error.param`. |
| `IDEMPOTENCY_CONFLICT`     | 409  | `conflict`             | Same `Idempotency-Key` reused with a different body. Use a fresh key OR don't change the body. See [Idempotency](/api-reference/api/idempotency).               |
| `RATE_LIMITED`             | 429  | `rate_limit`           | Back off using `Retry-After`. See [Rate limits](/api-reference/api/rate-limits).                                                                                |
| `NOT_IMPLEMENTED`          | 501  | `not_implemented`      | The endpoint stub is wired but the feature isn't shipped yet.                                                                                                   |
| `INTERNAL_ERROR`           | 500  | `internal_error`       | Unhandled server error. Include the `x-request-id` from the response when contacting support.                                                                   |

## Resource-specific codes

### Triggers (`/v1/automations/triggers`)

| Code                                | HTTP | Recovery                                                                                                                                                                                                                                                                                                      |
| ----------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TRIGGER_EVENT_NOT_FOUND`           | 404  | List with `GET /v1/automations/triggers` to find the id.                                                                                                                                                                                                                                                      |
| `TRIGGER_IMMUTABLE`                 | 422  | Integration trigger (Clerk / Stripe / …) — managed from the integration card on the Brew dashboard; the public API doesn't accept metadata edits to it. Connecting the integration provisions every supported event automatically; whether each event fires is controlled by which automations are published. |
| `TRIGGER_HAS_DEPENDENT_AUTOMATIONS` | 409  | Delete/detach the automations first. `details.referencingAutomations[]` lists each `{ automationId, name, published }`.                                                                                                                                                                                       |
| `PAYLOAD_SCHEMA_EMAIL_REQUIRED`     | 400  | Add `{ key: 'email', type: 'string', required: true }` to `payloadSchema.fields`. The email field is the contact key downstream automations route on.                                                                                                                                                         |

### Automations (`/v1/automations`)

| Code                           | HTTP | Recovery                                                                                                           |
| ------------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------ |
| `AUTOMATION_NOT_FOUND`         | 404  | List with `GET /v1/automations`.                                                                                   |
| `AUTOMATION_VERSION_NOT_FOUND` | 404  | Drop `automationVersionId` or use a known one (returned by `GET /v1/automations?automationId=…&include=versions`). |
| `AUTOMATION_NOT_PUBLISHED`     | 422  | Publish first before unpublishing.                                                                                 |
| `AUTOMATION_GRAPH_INVALID`     | 400  | Iterate over `details.issues` and fix each one — see the issue-kind table above.                                   |
| `PUBLISH_VALIDATION_FAILED`    | 409  | Iterate over `details.blockers[]`; each carries `nodeId`, `nodeLabel`, `message`.                                  |

### Automation runs (`/v1/automations/runs`)

| Code                       | HTTP | Recovery                                                                                           |
| -------------------------- | ---- | -------------------------------------------------------------------------------------------------- |
| `NO_PUBLISHED_AUTOMATION`  | 422  | At least one automation attached to the trigger must be `published: true` before a fire can match. |
| `AUTOMATION_RUN_NOT_FOUND` | 404  | List with `GET /v1/automations/runs` (paginated).                                                  |

### Emails (`/v1/emails`)

| Code                      | HTTP | Recovery                                                                                                                                         |
| ------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `EMAIL_NOT_FOUND`         | 404  | List with `GET /v1/emails`.                                                                                                                      |
| `EMAIL_VERSION_NOT_FOUND` | 404  | Drop `emailVersionId` or fetch a valid one.                                                                                                      |
| `EMAIL_NOT_READY`         | 422  | The email row's `status !== 'complete'` — the agent is still generating. Wait and retry.                                                         |
| `EMAIL_IN_PROGRESS`       | 409  | The email is currently `status: 'streaming'` (another `PATCH /v1/emails/{emailId}` is mid-flight). Wait and retry.                               |
| `EMAIL_ALREADY_SENT`      | 409  | An email can be sent exactly once. Duplicate the email row or generate a new one. `details.sendState` carries `{ sentAt, recipientCount, ... }`. |
| `BRAND_NOT_FOUND`         | 404  | The API key's brand was deleted. Re-issue the key.                                                                                               |
| `BRAND_NOT_READY`         | 422  | Brand extraction (logo / theme / domain) hasn't finished yet. Wait a few seconds and retry.                                                      |

### Sends (`/v1/sends`)

| Code                 | HTTP | Recovery                                                                                                     |
| -------------------- | ---- | ------------------------------------------------------------------------------------------------------------ |
| `SEND_NOT_FOUND`     | 404  | No send exists for that `sendId` on `GET /v1/analytics/sends?sendId=…`. List with `GET /v1/analytics/sends`. |
| `DOMAIN_NOT_FOUND`   | 404  | List with `GET /v1/domains`.                                                                                 |
| `DOMAIN_NOT_READY`   | 422  | The domain isn't verified for sending. Finish DNS verification in the dashboard.                             |
| `AUDIENCE_NOT_FOUND` | 404  | List with `GET /v1/audiences`. Provide an `audienceId` or inline `to` on `POST /v1/sends`.                   |

### Domains lifecycle (`/v1/domains`)

| Code                         | HTTP | Recovery                                                                                                 |
| ---------------------------- | ---- | -------------------------------------------------------------------------------------------------------- |
| `DOMAIN_ALREADY_EXISTS`      | 409  | The domain already exists for this brand — fetch it via `GET /v1/domains`.                               |
| `DOMAIN_VERIFIED_ELSEWHERE`  | 409  | Verified in another workspace; only unverified domains can be reclaimed.                                 |
| `DOMAIN_OTHER_BRAND`         | 409  | Attached to a different brand in this workspace — use that brand's key or remove it first.               |
| `DOMAIN_VERIFICATION_FAILED` | 422  | `PATCH { domainId, verify: true }` couldn't verify — publish the DNS `records` from the row, then retry. |
| `DOMAIN_PROVIDER_ERROR`      | 422  | The sending provider rejected the domain (e.g. not a registrable name). Fix `name` and retry.            |

### Audiences (`/v1/audiences`)

| Code                 | HTTP | Recovery                                                           |
| -------------------- | ---- | ------------------------------------------------------------------ |
| `AUDIENCE_NOT_FOUND` | 404  | Unknown / cross-brand `audienceId`. List with `GET /v1/audiences`. |

### Contacts + fields (`/v1/contacts`, `/v1/fields`)

| Code                   | HTTP | Recovery                                                               |
| ---------------------- | ---- | ---------------------------------------------------------------------- |
| `CONTACT_NOT_FOUND`    | 404  | Upsert the contact first via `POST /v1/contacts`.                      |
| `CORE_FIELD_IMMUTABLE` | 422  | Don't write read-only columns (e.g. `createdAt`, `email`).             |
| `FIELD_NOT_FOUND`      | 404  | Create the field first via `POST /v1/fields { fieldName, fieldType }`. |
| `MISSING_EMAIL`        | 422  | The single-row contact body requires `email`.                          |

## SDK error handling (TypeScript)

The official `@brew.new/sdk` throws a typed `BrewApiError` on every non-2xx response, exposing the full envelope:

```ts theme={null}
import { BrewApiError, createBrewClient } from '@brew.new/sdk'

const brew = createBrewClient({ apiKey: process.env.BREW_API_KEY! })

try {
  await brew.automations.triggers.fire({
    triggerEventId: 'tri_signup',
    payload: { email: 'jane@example.com' },
    idempotencyKey: `signup-${userId}-${eventTimestamp}`,
  })
} catch (err) {
  if (err instanceof BrewApiError) {
    console.error('Brew error', {
      code: err.code,                 // 'TRIGGER_EVENT_NOT_FOUND' etc.
      type: err.type,                 // 'not_found'
      message: err.message,
      requestId: err.requestId,       // include in support tickets
      param: err.param,
      suggestion: err.suggestion,
      docs: err.docs,
      retryAfter: err.retryAfter,
    })

    if (err.code === 'RATE_LIMITED') {
      // back off, then retry
    }
    if (err.code === 'AUTOMATION_GRAPH_INVALID') {
      for (const issue of err.details?.issues ?? []) {
        console.error(`  ${issue.kind} on ${issue.nodeId}: ${issue.message}`)
      }
    }
  }
  throw err
}
```

## Branching agent / SDK logic on `code`

Three rules:

1. **`code` is stable.** It's part of our public contract. We will not change the spelling of a code; we may add new ones.
2. **`type` is a coarse bucket** for default UX. Use `type === 'rate_limit'` to gate a retry; use `type === 'authentication_error'` to ask the user to re-issue the key.
3. **Never branch on `message`.** Operator-facing copy may change between releases.

## See also

* [Rate limits](/api-reference/api/rate-limits) — `429 RATE_LIMITED` + `Retry-After` cookbook.
* [Idempotency](/api-reference/api/idempotency) — `409 IDEMPOTENCY_CONFLICT` semantics.
* [Authentication](/api-reference/api/authentication) — `401` / `403` codes.
* [TypeScript SDK error handling](/sdks/typescript/error-handling) — patterns + retry helpers.

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