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.

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": [/* ... */] }
  }
}
FieldTypeWhen set
codestringAlways. Stable identifier — branch SDK logic on this.
typeenumAlways. One of authentication_error | authorization_error | invalid_request | not_found | conflict | rate_limit | not_implemented | internal_error.
messagestringAlways. Human-readable summary. Can change.
paramstring?When the failing field is known (e.g. payloadSchema.fields, nodes[0].config.emailVersionId).
suggestionstringAlways. Concrete recovery hint.
docsstringAlways. Deep-link to canonical reference.
retryAfterint?On 429. Mirrors the Retry-After header (seconds).
detailsobject?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." }
      ]
    }
  }
}
kindCause
duplicate_node_idTwo nodes share the same id.
connection_unknown_fromconnection.from points to a non-existent node.
connection_unknown_toconnection.to points to a non-existent node.
connection_targets_triggerA connection targets the trigger node (triggers are entry-only).
connection_self_loopconnection.from === connection.to.
email_not_foundemailVersionId doesn’t exist in this brand.
email_version_mismatchemailVersionId exists but belongs to a different emailId.
email_wrong_typeReferenced email is campaign — must be automation or transactional.
domain_not_founddomainId doesn’t exist in this brand.
domain_not_readyDomain not verified for sending.

Foundational error codes (every endpoint)

These can appear on any v1 endpoint:
CodeHTTPtypeRecovery
AUTHENTICATION_REQUIRED401authentication_errorSet the Authorization: Bearer <key> (or X-API-Key: <key>) header.
INVALID_API_KEY401authentication_errorThe key is malformed or unknown. Re-issue at brew.new/settings/api.
API_KEY_REVOKED401authentication_errorThe key was revoked. Re-issue.
INSUFFICIENT_PERMISSIONS403authorization_errorThe key lacks the route’s permission scope. error.param carries the missing scope name.
METHOD_NOT_ALLOWED405invalid_requestWrong verb on this resource.
INVALID_REQUEST400invalid_requestZod failure / unknown body key / malformed JSON / sending a removed field (e.g. provider on POST /v1/triggers). Fix the body per error.param.
IDEMPOTENCY_CONFLICT409conflictSame Idempotency-Key reused with a different body. Use a fresh key OR don’t change the body. See Idempotency.
RATE_LIMITED429rate_limitBack off using Retry-After. See Rate limits.
NOT_IMPLEMENTED501not_implementedThe endpoint stub is wired but the feature isn’t shipped yet (e.g. replay and cancel on /v1/automation/runs).
INTERNAL_ERROR500internal_errorUnhandled server error. Include the x-request-id from the response when contacting support.

Resource-specific codes

Triggers (/v1/triggers)

CodeHTTPRecovery
TRIGGER_EVENT_NOT_FOUND404List with GET /v1/triggers to find the id.
TRIGGER_DISABLED422Toggle on with PATCH /v1/triggers { triggerEventId, status: 'enabled' }.
TRIGGER_IMMUTABLE422Integration trigger (Clerk / Stripe / …) — manage from the integration only, not via the public API.
TRIGGER_HAS_DEPENDENT_AUTOMATIONS409Delete/detach the automations first. details.referencingAutomations[] lists each { automationId, name, published }.
PAYLOAD_SCHEMA_EMAIL_REQUIRED400Add { key: 'email', type: 'string', required: true } to payloadSchema.fields. The email field is the contact key downstream automations route on.

Automations (/v1/automations)

CodeHTTPRecovery
AUTOMATION_NOT_FOUND404List with GET /v1/automations.
AUTOMATION_VERSION_NOT_FOUND404Drop automationVersionId or use a known one (returned by ?include=versions).
AUTOMATION_NOT_PUBLISHED422Publish first before unpublishing.
AUTOMATION_GRAPH_INVALID400Iterate over details.issues and fix each one — see the issue-kind table above.
PUBLISH_VALIDATION_FAILED409Iterate over details.blockers[]; each carries nodeId, nodeLabel, message.

Automation runs (/v1/automation/runs)

CodeHTTPRecovery
NO_PUBLISHED_AUTOMATION422At least one automation attached to the trigger must be published: true before a fire can match.
AUTOMATION_RUN_NOT_FOUND404List with GET /v1/automation/runs (paginated).

Emails (/v1/emails)

CodeHTTPRecovery
EMAIL_NOT_FOUND404List with GET /v1/emails.
EMAIL_VERSION_NOT_FOUND404Drop emailVersionId or fetch a valid one.
EMAIL_NOT_READY422The email row’s status !== 'complete' — the agent is still generating. Wait and retry.
EMAIL_IN_PROGRESS409The email is currently status: 'streaming' (another PATCH /v1/emails is mid-flight). Wait and retry.
EMAIL_ALREADY_SENT409An email can be sent exactly once. Duplicate the email row or generate a new one. details.sendState carries { sentAt, recipientCount, ... }.
BRAND_NOT_FOUND404The API key’s brand was deleted. Re-issue the key.
BRAND_NOT_READY422Brand extraction (logo / theme / domain) hasn’t finished yet. Wait a few seconds and retry.

Sends (/v1/sends)

CodeHTTPRecovery
DOMAIN_NOT_FOUND404List with GET /v1/domains.
DOMAIN_NOT_READY422The domain isn’t verified for sending. Finish DNS verification in the dashboard.
AUDIENCE_NOT_FOUND404List with GET /v1/audiences. audienceId is required on /v1/sends.

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

CodeHTTPRecovery
CONTACT_NOT_FOUND404Upsert the contact first via POST /v1/contacts.
CORE_FIELD_IMMUTABLE422Don’t write read-only columns (e.g. createdAt, email).
FIELD_NOT_FOUND404Create the field first via POST /v1/fields { fieldName, fieldType }.
MISSING_EMAIL422The 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:
  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

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.