error envelope, including the trigger-fire endpoint, which now reports a bad
payload as 400 INVALID_PAYLOAD in that same envelope. Branch on the stable
code, never on a human-readable message that 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 | payment_required | service_unavailable | 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 retryable 429 or 503 responses. Mirrors the Retry-After header (seconds). |
details | object? | Machine-readable extras (e.g. details.blockers[], details.issues[]). |
#<code-in-kebab-case>, so
INSUFFICIENT_CREDITS is addressable as
/api-reference/api/errors#insufficient-credits.
Link those anchors from support threads, runbooks, and agent prompts. Today
the envelope’s docs value is the page URL; appending the per-code fragment
on the API side is an open question tracked in the repo notes.
details Shape: PUBLISH_VALIDATION_FAILED (422)
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.
{
"error": {
"code": "PUBLISH_VALIDATION_FAILED",
"type": "invalid_request",
"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. |
Error Code Catalog
Generated from the OpenAPI specification, so it lists every code the API can return and nothing it cannot. “Every operation” holds the codes any endpoint may answer with; the rest are grouped by the resource that raises them. Each code keeps its stable#<code-in-kebab-case> anchor.
Every operation
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
ACCOUNT_SUSPENDED | 403 | authorization_error | never | The organization behind the credential is suspended. | The organization behind this credential is suspended. Contact support@brew.new; do not retry. |
API_KEY_REVOKED | 401 | authentication_error | after fixing the request or resource | The API key was revoked. | Create a new active API key and retry. |
AUTHENTICATION_REQUIRED | 401 | authentication_error | after fixing the request or resource | No API key or session accompanied the request. | Provide a valid API key or sign in with an organization session. |
BRAND_ID_REQUIRED | 400 | invalid_request | after fixing the request or resource | An organization-scoped credential called a brand-scoped operation without naming the brand. | List brands with GET /v1/brands, then name one with the X-Brand-Id header. There is no default brand. |
BRAND_SCOPE_MISMATCH | 403 | authorization_error | after fixing the request or resource | A brand-scoped credential named a brand other than the one it is bound to. | Omit the brand to use the one this credential is bound to, or use an organization-scoped credential to reach other brands. |
IDEMPOTENCY_CONFLICT | 409 | conflict | after fixing the request or resource | The same Idempotency-Key was reused with a different request body. | Reuse the original payload or send a new idempotency key. |
IDEMPOTENCY_IN_PROGRESS | 409 | conflict | later (transient) | A request with this Idempotency-Key is still executing. | Wait for the original request to finish, then retry the same key to replay its response. |
INSUFFICIENT_CREDITS | 402 | payment_required | after fixing the request or resource | The credit balance is below what this operation requires; details.cost is the amount needed. | Upgrade your plan or wait for the next billing period to reset. Check your balance up front with GET /v1/usage. |
INSUFFICIENT_PERMISSIONS | 403 | authorization_error | after fixing the request or resource | The credential lacks the permission scope the operation needs. | Use an API key or session with the required permission. |
INSUFFICIENT_ROLE | 403 | authorization_error | after fixing the request or resource | The caller lacks the access the operation needs: param names member (access to the brand) or org_admin (the organization role). | Ask an organization admin to run this, to add you to the brand, or to make you an admin. |
INTERNAL_ERROR | 500 | internal_error | later (transient) | An unexpected failure; the x-request-id header identifies it. | Retry the request. If it keeps failing, contact support with the x-request-id header. |
INVALID_API_KEY | 401 | authentication_error | after fixing the request or resource | The API key is malformed or unknown. | Check the API key format and retry with a valid active key. |
INVALID_REQUEST | 400 | invalid_request | after fixing the request or resource | The body or query failed validation: an unknown key, a wrong type, or a missing required field. param names the offender. | Fix the field reported in param and retry. |
METHOD_NOT_ALLOWED | 405 | invalid_request | after fixing the request or resource | The path exists but not for this HTTP method; the Allow header lists the methods that do. | Use one of the methods named in the Allow header. |
NOT_FOUND | 404 | not_found | after fixing the request or resource | No v1 resource lives at this path. | Check the request path and HTTP method against the API reference. |
NOT_IMPLEMENTED | 501 | not_implemented | never | The operation is documented but not available yet. | Watch the changelog; do not retry. |
ORG_SCOPE_REQUIRED | 403 | authorization_error | after fixing the request or resource | The operation acts on the organization, so a brand-scoped credential cannot call it. | Create an organization-scoped API key in Settings, API, or reconnect MCP at the organization level. |
PAYLOAD_TOO_LARGE | 413 | invalid_request | after fixing the request or resource | The request body exceeds the route cap. | Reduce the payload size and retry. |
RATE_LIMITED | 429 | rate_limit | after Retry-After | The credential exhausted the rolling window for this route policy; Retry-After says when it reopens. | Wait for the retry window before sending another request. |
SERVICE_UNAVAILABLE | 503 | service_unavailable | after Retry-After | A dependency the operation must consult (billing, the idempotency store) is temporarily unavailable, so the request was refused rather than run unmetered. | Retry the request after a short delay. |
Audiences
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
AUDIENCE_BUILD_ACTIVE | 409 | conflict | later (transient) | The audience is being built, so neither it nor the hidden field its build stamps can be changed, copied, or deleted yet. | Wait for the build to finish (poll GET /v1/audiences/{audienceId}?include=build), then retry. |
AUDIENCE_BUILD_ALREADY_ACTIVE | 409 | conflict | later (transient) | Too many audience builds are already in progress for the workspace. | Wait for the in-flight builds to finish (poll GET /v1/audiences/{audienceId}?include=build), then retry. |
AUDIENCE_EDIT_CONFLICT | 409 | conflict | after fixing the request or resource | The audience changed since it was read; the update was not applied. | Reload the latest audience filters and retry. |
AUDIENCE_MEMBERSHIP_NOT_EXPRESSIBLE | 409 | conflict | after fixing the request or resource | The audience’s filters cannot add or remove these contacts exactly (for example, AND-combined conditions, or an OR branch that would still match). | Rewrite the audience with filters instead, or create a separate audience for these contacts. |
AUDIENCE_NOT_FOUND | 404 | not_found | after fixing the request or resource | No audience with that id exists in the brand. | Use GET /v1/audiences to choose a saved audience, or send to explicit addresses. |
Automations
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
AUTOMATION_GRAPH_INVALID | 400 | invalid_request | after fixing the request or resource | The nodes and connections do not form a valid graph; details.issues[] lists each problem with its nodeId and kind. | Fix every issue reported in details.issues then resubmit. |
AUTOMATION_NOT_FOUND | 404 | not_found | after fixing the request or resource | No automation with that id exists in the brand. | List automations with GET /v1/automations. |
AUTOMATION_NOT_PAUSABLE | 422 | invalid_request | after fixing the request or resource | The automation runs against a manual audience, so it is paused through its runs, not the automation. | Use the audience-run control operations to pause or cancel an in-flight run. |
AUTOMATION_NOT_PUBLISHED | 422 | invalid_request | after fixing the request or resource | The automation is not live, so there is nothing to unpublish. | Check the live state with GET /v1/automations/{automationId}. |
AUTOMATION_VERSION_CONFLICT | 409 | conflict | after fixing the request or resource | The automation changed since the version you edited was read. | Read the latest automation version, reapply the intended changes, and retry the update. |
AUTOMATION_VERSION_NOT_FOUND | 404 | not_found | after fixing the request or resource | The automation has no version with that id. | List versions with GET /v1/automations/{automationId}?include=versions. |
PUBLISH_VALIDATION_FAILED | 422 | invalid_request | after fixing the request or resource | The saved graph cannot go live; details.blockers[] lists what publish requires. | Fix every blocker reported in details.blockers then PATCH again with { published: true }. |
Brands
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
BRAND_DOMAIN_CONFLICT | 409 | conflict | after fixing the request or resource | The organization already has a brand for that domain. | Find the existing brand with GET /v1/brands. |
BRAND_LIMIT_REACHED | 402 | payment_required | after fixing the request or resource | The plan allows no more brands. | Delete an unused brand, or upgrade the plan to add more brands. |
BRAND_NOT_FOUND | 404 | not_found | after fixing the request or resource | The named or bound brand does not exist in this organization (unknown, deleting, or another organization). | List the brands this credential can reach with GET /v1/brands. |
BRAND_NOT_READY | 422 | invalid_request | later (transient) | The brand is still extracting and cannot generate email yet. | Poll GET /v1/brands/{brandId} until it reports status: completed, then retry. |
Chats
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
CHAT_NOT_FOUND | 404 | not_found | after fixing the request or resource | No chat with that id is visible to this credential’s brand. | Check the chatId, and that your key or connector is bound to that chat’s brand. |
Contacts
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
BATCH_TOO_LARGE | 422 | invalid_request | after fixing the request or resource | A batch write exceeds the per-request row cap. | Split the request into smaller batches and retry. |
CONTACT_NOT_FOUND | 404 | not_found | after fixing the request or resource | No contact with that email exists in the brand. | Create the contact first with POST /v1/contacts. |
INVALID_EMAIL | 422 | invalid_request | after fixing the request or resource | A contact email is not a deliverable address shape. | Fix the address named in param and retry. |
MISSING_EMAIL | 422 | invalid_request | after fixing the request or resource | A contact row has no email, the primary key. | Provide an email address for every contact in the request body. |
RESUBSCRIBE_NOT_ALLOWED | 422 | invalid_request | after fixing the request or resource | The write set subscribed to true on a contact who unsubscribed, and Brew never re-subscribes an opt-out through the API, MCP or an import; details.email names the contact. | Drop subscribed from the write; a contact who opted out re-subscribes through their own action, or from their contact page in the Brew app once they ask. |
Content
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
CONTENT_OPERATION_FAILED | 422 | invalid_request | after fixing the request or resource | The media operation could not be completed for the given input. | Check the input described in the message and retry. |
UPLOAD_IN_PROGRESS | 409 | conflict | later (transient) | Another request is still adding this uploaded image. | Wait a few seconds, then call POST /v1/content/add-image with the same uploadId to receive its result. |
UPLOAD_NOT_FOUND | 404 | not_found | after fixing the request or resource | The image upload is unknown, expired, or belongs to another brand. | Open a new upload with POST /v1/content/image-uploads, send the file, then add it within 15 minutes. |
UPLOAD_NOT_RECEIVED | 409 | conflict | after fixing the request or resource | The image upload has not received its file yet. | POST the raw file bytes to the uploadUrl, then call POST /v1/content/add-image with the same uploadId. |
Contracts
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
CONTRACT_LOCKED_BY_PUBLISHED_AUTOMATIONS | 409 | conflict | after fixing the request or resource | A published automation consumes this trigger, so the contract change must stay backward compatible: enforcement cannot tighten, a referenced field cannot be removed, retyped, or made required without a fallback, and an open object or list cannot gain a shape that drops a field a published automation reads. | Unpublish or detach the published automations that consume this trigger, or make a backward-compatible change (adding optional fields and loosening enforcement stay allowed). |
Domains
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
DOMAIN_ALREADY_EXISTS | 409 | conflict | after fixing the request or resource | The brand already has this domain. | Fetch the existing domain with GET /v1/domains. |
DOMAIN_CLAIMED_ELSEWHERE | 409 | conflict | after fixing the request or resource | The domain is registered in another Brew workspace. | Remove it from the other workspace first, or contact support to move it. |
DOMAIN_NOT_FOUND | 404 | not_found | after fixing the request or resource | No domain with that id exists in the brand. | Use GET /v1/domains to choose a verified domain. |
DOMAIN_NOT_READY | 422 | invalid_request | after fixing the request or resource | The domain is not verified for sending. | Use a verified domain, or finish domain verification before sending. |
DOMAIN_OTHER_BRAND | 409 | conflict | after fixing the request or resource | The domain is attached to a different brand in this workspace. | Use that brand’s credential, or remove the domain from it first. |
DOMAIN_PROVIDER_ERROR | 422 | invalid_request | after fixing the request or resource | The sending provider rejected the domain. | Check the domain name is a valid, registrable domain and retry. |
DOMAIN_PURPOSE_NOT_ALLOWED | 422 | invalid_request | after fixing the request or resource | The sending domain is admitted for a different purpose than this send (marketing vs transactional). | Use a marketing domain for campaigns and audience sends. For transactional flows, wire the design into a published automation with a trigger and fire POST /v1/automations/triggers/{triggerEventId}/fire. |
DOMAIN_VERIFICATION_FAILED | 422 | invalid_request | after fixing the request or resource | The DNS records are not published yet, so verification did not pass. | Confirm the DNS records from GET /v1/domains are published, then retry verify. |
DOMAIN_VERIFIED_ELSEWHERE | 409 | conflict | after fixing the request or resource | Another workspace already verified this domain. | Only unverified domains can be reclaimed. Remove it from the other workspace first. |
Emails
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
AUDIT_NOT_FOUND | 404 | not_found | after fixing the request or resource | No saved audit matches that id in this brand, or its seven day retention has expired. | Use an auditId returned by POST /v1/emails/audit for this brand, or run a new audit. |
EMAIL_GENERATION_FAILED | 502 | internal_error | later (transient) | The email agent could not produce a design for the prompt. | Retry the request. If it keeps failing, simplify or rephrase the prompt. |
EMAIL_GROUP_NAME_CONFLICT | 409 | conflict | after fixing the request or resource | A group with that name already exists in the brand. | Pick a different name, or rename the existing group. |
EMAIL_GROUP_NOT_FOUND | 404 | not_found | after fixing the request or resource | No email group with that id exists in the brand. | List groups with GET /v1/email-groups. |
EMAIL_IMPORT_FAILED | 422 | invalid_request | after fixing the request or resource | The supplied markup could not be imported as an email. | Check that content is valid for the declared format and retry. |
EMAIL_IN_PROGRESS | 409 | conflict | later (transient) | The design is being generated and cannot be edited or cloned yet. | Poll GET /v1/emails/{emailId} until status is ready, then retry. |
EMAIL_IN_USE_BY_AUTOMATION | 409 | conflict | after fixing the request or resource | A published automation sends this design, so it cannot be deleted. | Unpublish the automation (or swap the design on its send step), then retry the delete. |
EMAIL_NOT_FOUND | 404 | not_found | after fixing the request or resource | No email design with that id exists in the brand (cross-brand ids surface as 404). | List designs with GET /v1/emails. |
EMAIL_NOT_READY | 422 | invalid_request | later (transient) | The design is still generating or failed to generate, so it cannot be sent or cloned. | Use a completed design, or wait for generation to finish. |
EMAIL_RUN_AMBIGUOUS | 409 | conflict | after fixing the request or resource | A legacy generation run matches more than one saved version of the design. | List versions with GET /v1/emails/{emailId}?include=versions and read the one you want by emailVersionId. |
EMAIL_TEMPLATE_INVALID | 400 | invalid_request | after fixing the request or resource | The design’s template could not be resolved for the audit (unresolved merge tags or broken syntax); details carries the telemetry. | Fix the template syntax or add fallbacks for unresolved values, then run the audit again. |
EMAIL_VERSION_NOT_FOUND | 404 | not_found | after fixing the request or resource | The design has no version with that id. | List versions with GET /v1/emails/{emailId}?include=versions and retry with a valid emailVersionId. |
FLOW_NOT_FOUND | 404 | not_found | after fixing the request or resource | No public flow matches that slug: it is unknown, private, or one of its steps is no longer public. | List flows with GET /v1/flows and use a returned slug (the brand domain, for example notion.com). |
LIQUID_RENDER_ERROR | 422 | invalid_request | after fixing the request or resource | A Liquid template failed to parse or render. | Fix the template syntax, or supply the payload data the template references. Strict-mode fires fail on any unresolved variable; | default: is the sanctioned fallback. |
PREVIEW_NOT_FOUND | 404 | not_found | after fixing the request or resource | No rendering job matches that id in this brand, or its results have expired. | Use the previewId returned by POST /v1/emails/{emailId}/client-previews, or start a new job. |
REFERENCE_EMAIL_NOT_FOUND | 404 | not_found | after fixing the request or resource | The referenceEmailId names no design in the brand. | Check the referenceEmailId and retry with a valid email id. |
TEMPLATE_NOT_FOUND | 404 | not_found | after fixing the request or resource | No public template matches that id. | List templates with GET /v1/templates and use a returned emailId as the templateId. |
Fields
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
CORE_FIELD_IMMUTABLE | 422 | invalid_request | after fixing the request or resource | The field is a core contact column and cannot be created, changed, or deleted as a custom field. | Use a custom field name instead. |
FIELD_NOT_FOUND | 404 | not_found | after fixing the request or resource | No custom field definition with that name exists in the brand. | Create the field first with POST /v1/fields or use a field that already exists. |
FIELD_TYPE_MISMATCH | 409 | conflict | after fixing the request or resource | A value does not match the declared type of its custom field. | Send a value that matches the field’s type. To change the type, delete the field (this clears its value on every contact) and create it again. |
Figma
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
FIGMA_ACCESS_DENIED | 403 | authorization_error | after fixing the request or resource | The connected Figma account cannot read the file or frame. | Grant the connected Figma account access to the file and selected frame, then retry. |
FIGMA_CONVERSION_FAILED | 422 | invalid_request | after fixing the request or resource | The frame could not be converted into an email design; details.emailId references the partial design. | Confirm the frame is a self-contained email design, then retry. |
FIGMA_FRAME_NOT_FOUND | 404 | not_found | after fixing the request or resource | The file or frame no longer exists. | Confirm the file and frame still exist, then copy a fresh frame link from Figma. |
FIGMA_NOT_CONNECTED | 422 | invalid_request | after fixing the request or resource | No Figma account is connected to the brand. | Connect Figma on the Integrations page, then retry. |
FIGMA_UNAVAILABLE | 503 | service_unavailable | later (transient) | Figma did not answer in time. | Retry after a short delay. |
FIGMA_URL_INVALID | 400 | invalid_request | after fixing the request or resource | The Figma link is not a frame link with a node-id. | In Figma, select the email frame and copy its link. The URL must include a node-id. |
Integrations
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
EXPORT_PROVIDER_ERROR | 502 | service_unavailable | later (transient) | The connected provider refused or failed the template export; the status mirrors the provider reply. | Check the connection to the provider and retry the export. |
EXPORT_UNSUPPORTED | 422 | invalid_request | after fixing the request or resource | The export target rejected a field value it does not support. | Review the provider export fields and retry with a supported value. |
INTEGRATION_NOT_CONNECTED | 400 | invalid_request | after fixing the request or resource | The export target is not connected to this brand. | Connect the provider on the Integrations page, then retry the export. |
Runs
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
AUDIENCE_RUN_NOT_FOUND | 404 | not_found | after fixing the request or resource | No manual-audience run with that id exists in the brand. | List runs with GET /v1/automations/audience-runs. |
AUTOMATION_RUN_NOT_FOUND | 404 | not_found | after fixing the request or resource | No automation run with that id exists in the brand. | List runs with GET /v1/automations/runs. |
INVALID_PAYLOAD | 400 | invalid_request | after fixing the request or resource | The test payload does not match the trigger’s payload schema. | Send fields and types matching the trigger’s payloadSchema (see GET /v1/automations/triggers/{triggerEventId}). |
RUN_IN_PROGRESS | 409 | conflict | later (transient) | A manual-audience run is already in progress for this automation. | Wait for the in-progress run to finish (or cancel it) before running again. |
RUN_NOT_CANCELLABLE | 409 | conflict | never | The run already reached a terminal state. | The run has already finished and can no longer be canceled. |
RUN_NOT_PAUSABLE | 409 | conflict | after fixing the request or resource | Only a running manual-audience run can be paused. | Only a running run can be paused. |
RUN_NOT_PAUSED | 409 | conflict | after fixing the request or resource | Only a paused manual-audience run can be resumed. | Only a paused run can be resumed. |
RUN_NOT_RESUMABLE | 409 | conflict | after fixing the request or resource | The run cannot be resumed: a failed run needs an undelivered send step, and a run held at the plan limit resumes on its own. | For a failed run, start a new run for the remaining steps instead. A run held at the plan limit needs no call: it resumes once the monthly send allowance has room, or you can cancel it. |
RUN_START_FAILED | 503 | service_unavailable | later (transient) | The workflow engine could not start the run right now. | The workflow engine was momentarily busy; retry the run in a few seconds. |
RUN_STOP_FAILED | 503 | service_unavailable | later (transient) | The previous run of this cohort could not be stopped. | Retry the resume in a few seconds, or cancel the previous run first. |
Sends
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
CONSENT_REQUIRED | 422 | invalid_request | after fixing the request or resource | An inline marketing recipient has no contact record with marketing consent and the send carried no consent provenance; details.recipients lists the addresses. | Create the contacts first with POST /v1/contacts carrying a consent record, or include consent provenance on the send to create them as subscribed contacts. |
EMAIL_IMAGES_MISSING | 422 | invalid_request | after fixing the request or resource | The email shows an image on cdn.brew.new that does not exist, so it was not sent; details.missingImages[] lists each URL and the email it is in. | Replace or remove each image listed in details.missingImages in the email, then send again. |
NO_ELIGIBLE_RECIPIENTS | 422 | invalid_request | after fixing the request or resource | Every contact the send targets is unsubscribed, suppressed or undeliverable (or there are none), so an immediate send would deliver nothing; details.eligibility carries the counts. | Target an audience with subscribed, deliverable contacts, or schedule the send for a time when the audience will have them. |
RECIPIENT_UNSUBSCRIBED | 422 | invalid_request | after fixing the request or resource | An inline recipient is an existing contact who unsubscribed from marketing email; details.recipients lists the addresses and Brew never re-subscribes an opt-out. | Remove the unsubscribed addresses from the recipients; a contact re-subscribes only through their own action. |
SEND_NOT_CANCELLABLE | 409 | conflict | never | The send already finished, so there is nothing left to cancel. | Check the send status with GET /v1/sends/{sendId} before canceling. |
SEND_NOT_FOUND | 404 | not_found | after fixing the request or resource | No send with that id exists in the brand. | Use the sendId returned by POST /v1/sends, or list sends with GET /v1/sends. |
SEND_NOT_PAUSABLE | 409 | conflict | after fixing the request or resource | Only a delivering gradual send can be paused. | Check the send status with GET /v1/sends/{sendId}. |
SEND_NOT_RESUMABLE | 409 | conflict | after fixing the request or resource | Only a manually paused gradual send can be resumed. | Check the send status with GET /v1/sends/{sendId}. |
SEND_QUOTA_EXCEEDED | 402 | payment_required | after fixing the request or resource | The send or manual-audience run would exceed the plan’s monthly email volume (details carries used, limit, planKey and requested when known); the window resets with the billing period. | Upgrade your plan for a higher monthly send volume, or wait for the next billing period to reset. |
Triggers
| Code | HTTP | Type | Retry | What it means | What to do |
|---|---|---|---|---|---|
NO_PUBLISHED_AUTOMATION | 422 | invalid_request | after fixing the request or resource | The trigger fired, but no published automation listens for it, so nothing would run. | Publish an automation wired to this trigger, then fire again. GET /v1/automations/triggers/{triggerEventId}/readiness reports this before you send a live event. |
PAYLOAD_SCHEMA_EMAIL_REQUIRED | 400 | invalid_request | after fixing the request or resource | A trigger payload schema must declare a required string email field. | Add { key: “email”, type: “string”, required: true } to payloadSchema.fields before retrying. |
TRIGGER_ALREADY_EXISTS | 409 | conflict | after fixing the request or resource | A trigger with that id already exists in the brand. | Reuse the existing trigger, or choose a different triggerEventId. |
TRIGGER_EVENT_NOT_FOUND | 404 | not_found | after fixing the request or resource | No trigger with that id exists in the brand. | List triggers with GET /v1/automations/triggers. |
TRIGGER_HAS_DEPENDENT_AUTOMATIONS | 409 | conflict | after fixing the request or resource | Automations still reference the trigger; details.referencingAutomations[] names them. | Delete or detach the automations listed in details.referencingAutomations first, then retry DELETE. |
TRIGGER_IMMUTABLE | 422 | invalid_request | never | Integration-provisioned triggers cannot be changed through the API. | Only brew_api and custom triggers can be patched. Toggle per-event settings on the Integrations page instead. |
TRIGGER_INSTANCE_NOT_FOUND | 404 | not_found | after fixing the request or resource | No fired trigger instance with that id exists in the brand. | List recent fires with GET /v1/automations/trigger-instances. |
TRIGGER_LIMIT_REACHED | 409 | conflict | after fixing the request or resource | The brand already holds the maximum number of trigger events. | Delete an unused trigger with DELETE /v1/automations/triggers/{triggerEventId}, then retry. Each brand holds at most 100. |
Row-Level Codes
Batch contact writes (POST /v1/contacts, POST /v1/contacts/import-csv)
can answer 2xx and still reject individual rows. Those rejections arrive in
a per-row errors[] array rather than as the response status, and each entry
carries its own code:
| Code | Meaning |
|---|---|
INVALID_EMAIL | That row’s address is not a deliverable shape. Fix the row and resubmit it. |
DUPLICATE_EMAILS_IN_BATCH | The same address appears more than once in one request. Keep one row per email. |
summary and errors[] rather than
branching on the status alone.
Warnings
A2xx response can also carry a warnings[] array. A warning never fails
the request; it reports something you probably want to fix.
| Code | Operations | What it means |
|---|---|---|
CONSENT_RECORD_MISSING | createSend | An inline recipient is a subscribed contact with no consent record, so the send proceeds on subscription alone; field names the address and PATCH /v1/contacts/{email} records consent. |
CORE_FIELD_IGNORED | upsertContacts | A customFields key named a field Brew manages, such as createdAt, so its value was not stored while the rest of the contact was; field names the key, and sending the value under a custom field name keeps it. |
CSV_COLUMN_IGNORED | importContactsCsv | A CSV column was not imported, because the field it maps to is managed by Brew (such as createdAt) or because mapping fills the same core field from another column; field names the column, and mapping it to a custom field name keeps its values. |
DATE_ORDER_ASSUMED | upsertContacts, importContactsCsv | A date field held dates such as 03/04/2026 that read as either day/month or month/day, and nothing in the request settled the order, so they were read month/day, or in the order most of the field’s dates use; field names the field, and YYYY-MM-DD dates or dateOrder on a CSV import choose it. |
DRAFT_SAVED_NOT_LIVE | updateAutomation | The update was saved as a new draft version; live traffic still serves the previously published version until you republish. |
ENFORCEMENT_LOOSENED_WHILE_PUBLISHED | setTriggerContract | Enforcement was loosened while published automations consume the trigger; tightening it back will be refused with CONTRACT_LOCKED_BY_PUBLISHED_AUTOMATIONS until they are unpublished or detached. |
RECIPIENTS_EXCLUDED | createSend | Some contacts the send targets are unsubscribed, suppressed or undeliverable and will be skipped; the message gives the counts by reason. |
REQUIRED_FIELD_SATISFIED_BY_FALLBACK | setTriggerContract | A required field declares a fallbackValue, so a payload that omits it passes in every enforcement mode and every recipient receives the fallback; field names it. |
RESUBSCRIBE_SKIPPED | upsertContacts, importContactsCsv | A row set subscribed to true for a contact who unsubscribed, so its other fields were saved but the contact stays unsubscribed; email names the contact. |
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.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:
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. - Organization and brand credentials: why
ORG_SCOPE_REQUIRED,INSUFFICIENT_ROLE, andINSUFFICIENT_PERMISSIONSare three different problems. - Migrate to the new v1 surface: the codes and statuses the v1 cleanup renamed.
- 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 explore it further with ChatGPT or Claude.
Schedule a Call
Book time with our founders for personalized guidance on strategy, best practices, or complex implementation questions.
Call Us Directly
Need immediate assistance? Reach us at +1-(332)-203-2145 for urgent issues or time-sensitive questions.
Slack Channel
Our preferred support channel. You’ll receive an invite after signup for direct founder support and fast responses.
Email Support
Contact us at support@brew.new for detailed inquiries or if you prefer not to use Slack.