Overview
The Brew API lets you build email programs without touching the dashboard. Generate emails, manage contacts, fire automations, and send campaigns, all from your backend. The surface is organized around four ideas:- Emails are pure designs. A design carries no send state and no type. Generate one, edit it, restore old versions, then send it as many times as you like.
- A send is the unit of delivery + analytics. Every send
(campaign or automation) is one
sendId, the thing you poll for status and the key every delivery event attaches to. - Analytics is read-only reporting derived from sends + their per-recipient events.
- Automations wire a trigger to a graph of
sendEmail/wait/filter/splitnodes that deliver per-recipient when the trigger fires.
- Generate, import, and edit emails with AI (
/v1/emails). - Send an email to a saved audience, an inline recipient list, or a
single address (
POST /v1/sends), and QA it first with the same endpoint and{ test: true }. - Read delivery + engagement analytics
(
/v1/analytics/sends,/v1/analytics/campaigns,/v1/analytics/automations,/v1/analytics/events). - Define event triggers (
/v1/automations/triggers), assemble automation graphs (/v1/automations), publish them, and fire them (/v1/automations/triggers/{triggerEventId}/fire). - Manage contacts, custom fields, audiences, and domains.
Base URL
/v1 prefix.
Discover the API Programmatically
Three no-auth discovery endpoints let an agent learn the whole surface before it has a key:GET /v1/help and GET /v1/llms.txt are the machine + prose mirror
of this page; keep them handy when wiring an agent.
Authentication
Every request needs a Brew API key. Create and manage keys at brew.new/settings/api. You can send it in either header:GET /v1/brand→{ brand }is the brand the key is pinned to. Checkreadybefore generating emails.GET /v1/usage→{ plan, credits, emailSends, period }is the billing + quota surface (credit balance and send allowance).
Brand Scoping
Every API key is bound to exactly one brand at creation time. The brand is resolved from the key on every request: no public endpoint accepts abrandId field in its request body or query
string. To operate on a different brand, switch brands in the
dashboard at
brew.new/settings/api and create a
new key for that brand.
Reads filter to the key’s brand automatically; mutations write only
to the key’s brand. Cross-brand identifiers surface as 404 (not
403) so the API never confirms the existence of resources in
another brand. The only organization-wide resource is the public
template gallery (GET /v1/templates).
If you send a brandId field (body or query) to any /v1/*
endpoint, the request fails with 400 INVALID_REQUEST and
param: "brandId". Strip the field; the brand always comes from the
key.
Permission Scopes
Each API key carries a list of permission scopes. The dashboard defaults new keys toall. The coarse scopes imply the granular
ones: contacts ⊇ audiences; emails ⊇ domains + sends.
Mint a least-privilege key when you only need one resource.
Sending a request with a key that lacks the required permission
returns
403 INSUFFICIENT_PERMISSIONS with the missing scope name
in the error envelope’s param field. See
Authentication for the per-route
scope table.
How the API Fits Together
The surface is built around two delivery modes:- One-shot campaigns: generate (or pick) a design, then
POST /v1/sendswith either a brand-ownedaudienceIdor an inlinetolist (≤ 50). The workflow runtime fans out to every recipient and mints onesendIdyou poll for status + stats. - Event-driven automations: define a
trigger, generate the design bodies, assemble anautomationgraph that wires the trigger to one or moresendEmailnodes, publish it, and firePOST /v1/automations/triggers/{triggerEventId}/firefrom your backend whenever the real event happens. The runtime delivers per-recipient, onesendIdper recipient.
sends entity, discriminated by a
kind of campaign or automation. Analytics reads from there.
Resource Reference
The surface lives in three top-level domains plus the supporting resources (emails, contacts, audiences, domains, brand, templates).Emails: /v1/emails
AI-generated email designs, plus the action that delivers them. A
design carries no type and no send state: any design in the
brand can be sent to a target or referenced by an automation
sendEmail node, any number of times.
POST /v1/emails: generate from{ prompt, contentUrls?, referenceEmailId? }. Returns201 { emailId, emailVersionId, html, previewImage? }, or200 { response }when the agent answered in prose. Credit-metered.POST /v1/emails/import: ingest existing markup{ format: 'html' | 'mjml' | 'jsx', content, title?, baseUrl? }into an editable design (external images are re-hosted on the CDN). Returns201 { emailId, emailVersionId, html, previewImage? }. Usage-metered.POST /v1/emails/figma: transpile a Figma frame into an editable design{ figmaUrl, title?, format?: 'jsx' | 'html' }. ThefigmaUrlmust include anode-id, i.e. the link to one frame. It requires the API-key brand’s connected Figma integration; credentials never travel in the request. Returns201 { emailId, emailVersionId, title, format, content, warningCount, exportedNodeCount, previewImage? }. Deterministic, so no model runs and it is free.GET /v1/emails: list designs (filterstatus,createdAtFrom/To,updatedAtFrom/To). Pass?emailId=for one design + its renderedhtml; the row already carriespreviewImage. Add?include=html,versionsto embed the rendered HTML and the version history.PATCH /v1/emails/{emailId}: AI edit{ prompt, emailVersionId? }→ new latest version (usage-metered).DELETE /v1/emails/{emailId}: delete every version.POST /v1/emails/{emailId}/restore:{ version }clones a numbered version into a new latest.POST /v1/emails/{emailId}/accessibility-audit: WCAG 2.1 audit (fixed credit cost).POST /v1/emails/{emailId}/client-previews: render the design in real inboxes/devices (Gmail, Outlook, Apple Mail, iOS, light & dark) → a screenshot per client. Fixed credit cost, charged only when at least one screenshot is produced.
previewImage, ?include=versions
returns history, on-demand rendering is POST /v1/content/html-to-png,
and a design’s send history is GET /v1/analytics/sends?emailId=.
Sending (/v1/sends). Delivering a design is action-only: it
starts a delivery; the reads live under Analytics. Sending is not
campaign-specific: send the same design to a saved audience, an
inline list, or a single address, as many times as you like.
POST /v1/sends: polymorphic bytest. A campaign send{ emailId, emailVersionId?, domainId, subject, previewText?, replyTo?, (audienceId | to), scheduledAt? }provides exactly one ofaudienceId(a brand-owned saved audience) orto(an inline email or array of ≤ 50) and returns202 { status: 'queued' | 'scheduled', sendId, runId, scheduledAt? }. A test send{ test: true, emailId, emailVersionId?, subject, previewText?, to, replyTo? }does a one-off[TEST]delivery to a single inbox, forced Brew sender, no verified domain, audience, or send row, and resolves synchronously →200 { status: 'sent', recipient }.POST /v1/sends/{sendId}/cancel: cancel a scheduled or queued send before it goes out →200 { sendId, status: 'canceled' }. Idempotent (an already-canceledsend returns200); once the send issending,sent, orfailedit is409 SEND_NOT_CANCELLABLE, and an unknown / cross-brand id is404 SEND_NOT_FOUND.
Analytics: /v1/analytics
Every analytics read is emails-scoped except the automation +
trigger feeds, which are automations-scoped.
GET /v1/analytics/sends: the single send read. List every send (filterstatus,from,to), narrow to one design with?emailId=, or fetch one send with?sendId=; poll its lifecyclestatus+ aggregatedstats. Add?include=eventsto inline the per-recipient event feed (sent/delivered/opened/clicked/bounced/complained/unsubscribed).GET /v1/analytics/campaigns: lifetime KPIs per campaign send (one row persendId).GET /v1/analytics/automations: windowed per-automation performance + totals (default last 30 days).automationsscope.GET /v1/analytics/events: unified event feed across domains (filterrecipientEmail,eventType,automationId,sendId,from,to).GET /v1/analytics/trigger-instances: audit log of fired trigger instances (filter?triggerEventId=, or?triggerInstanceId=for one fired-event row: state, attempts, matched automations, started runs).automationsscope.
Automations: /v1/automations
Versioned graphs of nodes (trigger / sendEmail / wait / filter /
split). The public surface is deterministic: every body carries
the explicit { nodes, connections } graph.
POST /v1/automations: create{ name, description?, triggerEventId, nodes, connections, dryRun? }.dryRun: truevalidates without writing.GET /v1/automations: list (lean rows; graph omitted). Pass?automationId=for one automation, and?include=graph,versionsto embed the full{ nodes, connections }graph and its version history.PATCH /v1/automations/{automationId}: updatename/description/nodes/connections/triggerEventId, or flip the lifecycle with{ published }.{ published: true, automationVersionId? }validates the graph and goes live (409 PUBLISH_VALIDATION_FAILEDif it fails);{ published: false }stops matching fires (422 AUTOMATION_NOT_PUBLISHEDif it was never published).publishedis mutually exclusive with any field / graph update. Send it on its own.DELETE /v1/automations/{automationId}: cascade delete (versions- runs + logs; the referenced designs survive).
POST /v1/automations/{automationId}/test: suppression-aware test run{ payload? }→202.
sendEmail node requires emailId, emailVersionId,
domainId, subject, previewText. The server-side graph resolver
(AUTOMATION_GRAPH_INVALID) verifies each foreign key + structural
constraint before any write. Each sendEmail node creates one
sends row per recipient that flows through it.
Triggers: /v1/automations/triggers
Brand-scoped event definitions. A trigger is a payloadSchema
contract plus a stable triggerEventId. Triggers created via this
resource are hardcoded to provider: 'brew_api'; integration
triggers (clerk, stripe, shopify, …) are provisioned by the
corresponding integration and listed here read-only.
POST /v1/automations/triggers: create{ title, description?, payloadSchema }(noprovider/providerEventKey. Those are rejected).GET /v1/automations/triggers: list every trigger; pass?triggerEventId=for one trigger row.PATCH /v1/automations/triggers/{triggerEventId}: updatetitle/description/payloadSchema. Triggers have no status field; whether one fires is gated by the bound automation being published.DELETE /v1/automations/triggers/{triggerEventId}: delete (refused with409 TRIGGER_HAS_DEPENDENT_AUTOMATIONSwhile a non-archived automation references it).POST /v1/automations/triggers/{triggerEventId}/fire: fire{ payload, idempotencyKey? }. Starts every published automation on the trigger. This is the one endpoint that returns the legacy fire envelope. Read run ids fromdetails.automationRunIds.
Automation Runs: /v1/automations/runs
Read-only view of the workflow runs created by firing a trigger or
test-running an automation.
GET /v1/automations/runs: list (filterautomationId,triggerEventId,triggerInstanceId,recipientEmail,status,mode,from,to). Pass?automationRunId=for one run, and?include=logsto attach its per-nodelogs[].
Contacts + Fields: /v1/contacts, /v1/fields
Contacts are keyed on email and live under one brand; customFields
columns are declared via /v1/fields. The contact read is a single
endpoint, POST /v1/contacts/search { filters, audienceId?, search?, sort, count?, cursor }, and a by-email lookup is just a
{ field: 'email', operator: 'equals', value } filter. Writes are
path-based: POST /v1/contacts upserts a single contact or a batch
(≤ 1000), and PATCH / DELETE /v1/contacts/{email} update or remove
one. Contacts also offer deliverability validation
(POST /v1/contacts/validate), CSV import (POST /v1/contacts/import-csv),
and batch delete (POST /v1/contacts/batch-delete).
POST /v1/contacts/validate writes the verdict back onto matching
contacts (it is no longer read-only), and both the upsert (POST /v1/contacts)
and CSV import accept an optional validate: true to deliverability-check each
address as it’s ingested, 2 credits per address charged only on success. Up to
100 addresses validate inline and the response carries a validation count
summary; larger submissions upsert first and validate as a background job,
returning a validationJobId. On the contact itself the verdict field is
validationStatus (valid / risky / invalid); the legacy
verificationStatus is still dual-emitted with the same value for back-compat.
Audiences + Domains: /v1/audiences, /v1/domains
Brand-owned saved audiences (full CRUD; read one with
GET /v1/audiences?audienceId=, and add &include=count to make the
row’s count field the live total) and sending domains (list with
GET /v1/domains, one with ?domainId=, ?sendableOnly=true to
filter to verified senders; add → verify → set sender defaults →
delete).
Brand: /v1/brand
Read the key’s brand (GET /v1/brand, check ready) and the design
system the agent follows. Sub-resources fold into the brand row:
GET /v1/brand?include=identity,emailDesign,imageStyle,logos expands
the requested facets inline, and PATCH /v1/brand { identity?, emailDesign?, imageStyle? } writes them back. The brand image library
is GET /v1/brand/images, dual-mode: pass ?q= for credit-metered
semantic search (also ?type= / ?aspectRatio=), or omit it to browse
the library for free.
Templates: /v1/templates
Public template gallery. GET /v1/templates returns full rows:
each carries html, previewImage, title, category, brand,
and updatedAt. Filter on brand, category, semantic. Pass a
template’s emailId as referenceEmailId to POST /v1/emails to
seed generation.
Quick Start
1
Get an API key
Go to brew.new/settings/api and
create a key. The key is bound to whatever brand is active in the
dashboard at creation time, so switch brands first if you want a
key for a different brand.
2
Check that auth works
Read the brand pinned to the key:
3
Generate a design
Create a saved design from a prompt:
4
Send to an audience
Use the returned Then poll
emailId, a verified domainId, and a saved
audienceId:GET /v1/analytics/sends?sendId=… for status + stats.5
Send it
List your verified domains and saved audiences, then
POST /v1/sends
with the emailId from step 3:6
(or) Fire an automation
Build an automation graph against a trigger, publish it, then
fire it from your backend whenever the event happens:
Current Public v1 Surface
Idempotency
POST endpoints support idempotency via the Idempotency-Key header
(≤ 100 chars). Replays with the same body return the cached
response for 24 h; replays with a different body return
409 IDEMPOTENCY_CONFLICT.
POST /v1/automations/triggers/{triggerEventId}/fire also
honors a body idempotencyKey field for legacy back-compat. Prefer
the header.
Use it on every POST your code might retry, especially the fire
endpoint, so a double-fired webhook doesn’t double-deliver. See
Idempotency for the full contract.
Credits
Credit-metered routes use two modes. Usage-metered ops:POST /v1/emails, POST /v1/emails/import, AI edits via
PATCH /v1/emails/{emailId}, GET /v1/brand/images?q= (semantic
search), and POST /v1/content/generate-image, charge the actual
model usage (tokens / image cost), with no fixed number and no
X-Credit-Cost header. Fixed ops, the other /v1/content/*
operations and POST /v1/emails/{emailId}/client-previews, charge a
flat, published cost reported via X-Credit-Cost
/ X-Credits-Remaining headers. Both gate
on balance: a call against an empty balance returns
402 INSUFFICIENT_CREDITS. Check your balance up front with
GET /v1/usage. See Credits.
Rate Limits and Debugging Headers
Every response carries:x-request-id: opaque correlator. Always include this when you contact support.X-RateLimit-Limit: requests allowed in the current 60s window.X-RateLimit-Remaining: requests left.X-RateLimit-Reset: unix epoch seconds when the window resets.
429 RATE_LIMITED also sets Retry-After: <seconds>. Limits
are per API key, per route, per 60s window. UI-backed session traffic
gets a 300/min ceiling across the board.
See Rate limits for the full
per-route policy table and a 429 recovery cookbook, and
Response headers for every
header Brew sets.
TypeScript SDK
If you prefer typed wrappers over raw HTTP, Brew also ships an official TypeScript SDK at@brew.new/sdk. Every resource has a
matching client method (brew.emails.generate, brew.emails.send,
brew.analytics.sends.list, brew.automations.create,
brew.automations.triggers.fire, …).
TypeScript SDK
Use
@brew.new/sdk for typed requests, retries, idempotency, and a
resource-oriented client surface.Agentic Cookbook
End-to-end recipes for AI agents wiring designs → sends →
analytics → automations.
What to Read Next
Public API v1
Browse every generated endpoint page from the current OpenAPI spec.
SDK Overview
Start with the official TypeScript SDK.
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.