error.code (stable) — not on error.message (human-readable, can change).
The error envelope
| 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.
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:
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. PATCH /v1/automation/runs cancel). Replay (POST { automationRunId, mode: 'replay' }) is now implemented. |
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_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 ?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 |
|---|---|---|
SEND_NOT_FOUND | 404 | No send exists for that emailId on GET /v1/sends?emailId=. List with GET /v1/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. audienceId is required on a campaign /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:
Branching agent / SDK logic on code
Three rules:
codeis stable. It’s part of our public contract. We will not change the spelling of a code; we may add new ones.typeis a coarse bucket for default UX. Usetype === 'rate_limit'to gate a retry; usetype === 'authentication_error'to ask the user to re-issue the key.- Never branch on
message. Operator-facing copy may change between releases.
See also
- Rate limits —
429 RATE_LIMITED+Retry-Aftercookbook. - Idempotency —
409 IDEMPOTENCY_CONFLICTsemantics. - Authentication —
401/403codes. - TypeScript SDK 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:- Self-Service Tools
- Talk to Our Team
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.