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.
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
{
"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 { published: true } is blocked, details.blockers[] enumerates every node-level reason so callers can render a fix-it list.
{
"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:
{
"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. |
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/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. |
RATE_LIMITED | 429 | rate_limit | Back off using Retry-After. See Rate limits. |
NOT_IMPLEMENTED | 501 | not_implemented | The endpoint stub is wired but the feature isn’t shipped yet (e.g. replay and cancel on /v1/automation/runs). |
INTERNAL_ERROR | 500 | internal_error | Unhandled server error. Include the x-request-id from the response when contacting support. |
Resource-specific codes
Triggers (/v1/triggers)
| Code | HTTP | Recovery |
|---|
TRIGGER_EVENT_NOT_FOUND | 404 | List with GET /v1/triggers to find the id. |
TRIGGER_DISABLED | 422 | Toggle on with PATCH /v1/triggers { triggerEventId, status: 'enabled' }. |
TRIGGER_IMMUTABLE | 422 | Integration trigger (Clerk / Stripe / …) — manage from the integration only, not via the public API. |
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 ?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/automation/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/automation/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 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 |
|---|
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. audienceId is required on /v1/sends. |
| 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:
import { BrewApiError, createBrewClient } from '@brew.new/sdk'
const brew = createBrewClient({ apiKey: process.env.BREW_API_KEY! })
try {
await brew.automationRuns.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:
code is stable. It’s part of our public contract. We will not change the spelling of a code; we may add new ones.
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.
- Never branch on
message. Operator-facing copy may change between releases.
See also
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: