Overview
Brew Public API v1 gives you programmatic access to the same core flows that power the product — define event triggers, generate emails, assemble automation graphs, fire automation runs, send campaigns, and manage contacts. Use it to:- Define event triggers (
/v1/triggers) and fire them from your backend (/v1/automation/runs). - Generate and edit emails with AI (
/v1/emails). - Author automation graphs that wire triggers to one or more
sendEmail/wait/filter/splitnodes (/v1/automations). - Publish automations and inspect each automation run + per-node
logs (
/v1/automation/runs). - Send a one-shot campaign to a saved audience (
/v1/sends). - Manage contacts, custom fields, audiences, and domains.
Base URL
/v1 prefix.
Authentication
Every request needs a Brew API key. Create and manage keys at brew.new/settings/api. You can send it in either header: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.
| Scope | Endpoints | Behavior |
|---|---|---|
| Brand-scoped (filtered to the key brand automatically) | /v1/triggers, /v1/automations, /v1/automation/runs, /v1/contacts, /v1/fields, /v1/audiences, /v1/domains, /v1/emails, /v1/sends, /v1/events (legacy) | Reads filter to the key brand. Mutations write only to the key brand. Cross-brand identifiers surface as 404 (not 403) so the API never confirms the existence of resources in another brand. |
| Organization-wide | GET /v1/templates | Returns the public template catalog. |
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.
Brands themselves are managed only in the Brew dashboard. The public
API does not expose endpoints to create, list, or update brands.
Permission Scopes
Each API key carries a list of permission scopes. The dashboard defaults new keys toall. Routes require either the listed scope
or all:
| Permission | Endpoints |
|---|---|
contacts | /v1/contacts, /v1/fields, /v1/audiences |
emails | /v1/domains, /v1/emails, /v1/templates |
sends | /v1/sends |
automations | /v1/triggers, /v1/automations, /v1/automation/runs, /v1/events (legacy) |
all | every endpoint above |
403 INSUFFICIENT_PERMISSIONS with the missing scope name
in the error envelope’s param field.
How The API Fits Together
The flat surface is built around two delivery modes:- Event-driven automations — define a
trigger, mint the email bodies, assemble anautomationgraph that wires the trigger to one or moresendEmailnodes, publish it, and firePOST /v1/automation/runsfrom your backend whenever the real event happens. The workflow runtime delivers per-recipient. - One-shot audience campaigns — generate (or pick) an email,
then
POST /v1/sendswith a brand-ownedaudienceId. The workflow runtime fans out to every audience member.
Resource Reference
Triggers — /v1/triggers
Brand-scoped event definitions. A trigger is a payloadSchema
contract plus a stable triggerEventId. Every trigger created via
this resource is hardcoded to provider: 'brew_api'; integration
triggers (clerk, stripe, shopify, …) are provisioned by the
corresponding integration and listed here read-only.
POST /v1/triggers— deterministic create. Body:{ title, description?, payloadSchema }(noprovider/providerEventKey— those are rejected).GET /v1/triggers— always returns{ triggers: TriggerRow[] }.?triggerEventId=…returns a one-element list.PATCH /v1/triggers— update trigger metadata (title,description,payloadSchema). Trigger rows don’t have a status field; whether a trigger fires is controlled by the bound automation being published. Sending{ status }returns400 INVALID_REQUEST.DELETE /v1/triggers— delete (refused with409 TRIGGER_HAS_DEPENDENT_AUTOMATIONSif non-archived automations still reference it).
Emails — /v1/emails
AI-generated email bodies. Every email row carries an emailType:
emailType | Where it shows | Used by |
|---|---|---|
campaign | /emails canvas (default board) | POST /v1/sends. |
automation | Hidden from /emails canvas | sendEmail nodes inside an automation graph. |
transactional | /emails canvas (default board) | Both POST /v1/sends and automation sendEmail references. |
GET /v1/emails— list latest emails, filterable byemailType.POST /v1/emails— generate (required body{ prompt, emailType, contentUrl?, referenceEmailId? }). Response is aGeneratedEmailArtifact(withemailId+emailVersionId) or aTextResponse(the agent answered with prose).PATCH /v1/emails— edit an existing email. OptionalemailVersionIdpins the edit to a specific source version.
Automations — /v1/automations
Versioned graphs of nodes (trigger / sendEmail / wait / filter /
split). Public surface is deterministic-only — every body
carries the explicit { nodes, connections } graph.
POST /v1/automations— create.dryRun: truevalidates without writing.GET /v1/automations— always returns{ automations: AutomationRow[] }.?include=versions(single-row mode) attachesversions[]inline on the row.PATCH /v1/automations— update OR publish (published: boolean). Publishing runsvalidateAutomationForPublishand returns409 PUBLISH_VALIDATION_FAILEDwithdetails.blockers[]when not ready.DELETE /v1/automations— cascade.
sendEmail node requires emailId, emailVersionId,
domainId, subject, previewText at the API authoring layer.
The server-side graph resolver (AUTOMATION_GRAPH_INVALID)
verifies each FK + structural constraint before any write.
Automation runs — /v1/automation/runs
Workflow runs created by firing a trigger or test-running an
automation. POST body is a 3-branch union:
- Fire —
{ triggerEventId, payload, idempotencyKey?, dryRun? }starts a workflow per published automation attached to the trigger. Response carriesdetails.automationRunIds[]. - Test —
{ automationId, mode: 'test', payload? }runs the automation in test mode (no real mail). - Replay —
{ automationId, triggerInstanceId, mode: 'replay' }(P7 — currently501 NOT_IMPLEMENTED).
GET /v1/automation/runs always returns
{ runs: AutomationRunRow[] }. Filter on
automationId / triggerEventId / triggerInstanceId / recipientEmail / status / mode / from / to / limit / cursor. ?include=logs
attaches per-node logs[].
PATCH /v1/automation/runs (cancel) is currently 501 NOT_IMPLEMENTED.
Sends — /v1/sends
Campaign-only one-shot send. audienceId is required — the
public surface only supports audience-scoped blasts. For
per-recipient event-driven delivery use POST /v1/automation/runs
(fire branch) against a published automation graph instead.
Contacts + fields — /v1/contacts, /v1/fields
CRUD for recipient data. Contacts are keyed on email and live
under one brand. customFields columns are declared via
/v1/fields. Both endpoints support single + batch shapes (up to
1000 rows per batch).
Audiences + domains — /v1/audiences, /v1/domains
Read-only listings of brand-owned audiences and verified sending
domains. Both are managed in the dashboard.
Templates — /v1/templates
Read-only listing of curated starter emails. Pass template.emailId
as referenceEmailId to POST /v1/emails to seed generation.
Legacy — /v1/events, /v1/executions
Both URLs now forward to /v1/automation/runs. Wire shape is
identical to the canonical successor; only the response headers
change — every reply carries:
/v1/automation/runs before the sunset. See
Legacy aliases below for the full per-route
matrix.
Quick Start
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 — switch brands first if you want a key
for a different brand.
Current Public v1 Surface
| Method | Endpoint | Purpose |
|---|---|---|
POST | /v1/triggers | Create a custom brew_api trigger (deterministic). |
GET | /v1/triggers | List every trigger in the brand (always { triggers: [...] }). |
PATCH | /v1/triggers | Update trigger metadata (title, description, payloadSchema). Triggers don’t have a status field. |
DELETE | /v1/triggers | Delete a trigger (refused if dependent automations exist). |
POST | /v1/emails | Generate a new email (required emailType). |
GET | /v1/emails | List emails, filterable by emailType + status + timestamps. |
PATCH | /v1/emails | Edit an email. Optional emailVersionId pin. |
POST | /v1/automations | Create an automation graph. dryRun: true validates only. |
GET | /v1/automations | List automations (always { automations: [...] }). ?include=versions on single-row. |
PATCH | /v1/automations | Update graph OR publish/unpublish. |
DELETE | /v1/automations | Cascade-delete automation + versions + runs + logs. |
POST | /v1/automation/runs | Fire / test / replay (body-discriminated). |
GET | /v1/automation/runs | List runs (always { runs: [...] }). ?include=logs attaches logs[]. |
PATCH | /v1/automation/runs | Cancel an in-flight run (P7 — 501 today). |
POST | /v1/sends | Start a campaign send to a brand-owned audience. |
GET | /v1/contacts | List / count / lookup contacts. |
POST | /v1/contacts | Upsert one or many contacts. |
PATCH | /v1/contacts | Patch a contact. |
DELETE | /v1/contacts | Delete one or many contacts. |
GET | /v1/fields | List custom contact field definitions. |
POST | /v1/fields | Create a contact field. |
DELETE | /v1/fields | Delete a contact field. |
GET | /v1/audiences | List saved audiences. |
GET | /v1/domains | List verified sending domains. |
GET | /v1/templates | List public templates. |
/v1/events,
/v1/executions, /v1/triggers/{id},
/v1/automations/{id}, /v1/emails/{emailId}, and the surviving
action sub-paths (/publish, /test, /versions) forward to the
flat handlers with Deprecation / Sunset / Link headers. The
trigger-status routes (POST /v1/triggers/{id}/enable and
/disable) were removed in the one-switch refactor and now return
404 — triggers are always live, fire is gated by the bound
automation being published.
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/automation/runs (fire branch) additionally honours a body
idempotencyKey field for back-compat with the legacy /v1/events
route.
Use it on every POST that your code might retry — especially
POST /v1/automation/runs (fire) so a doubled-fired webhook doesn’t
double-deliver. See Idempotency for
the full contract + cookbook.
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 additionally 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.
Legacy aliases
The flat/v1/* surface is the canonical contract. A handful of
older URL shapes ship as deprecated aliases for one release
window so existing integrations keep working through the cutover —
each forwards to the canonical successor and adds three response
headers on every reply:
Sunset timestamp to drop the deprecation noise.
Wire shape is identical between alias and successor; the only
difference is the response headers.
| Legacy route | Canonical successor |
|---|---|
POST/GET/PATCH /v1/executions | POST/GET/PATCH /v1/automation/runs |
POST /v1/events | POST /v1/automation/runs (fire branch) |
GET /v1/events[/{triggerInstanceId}] | GET /v1/automation/runs?triggerInstanceId=… |
PATCH /v1/emails/{emailId} | PATCH /v1/emails body { emailId, … } |
GET/PATCH/DELETE /v1/triggers/{triggerEventId} | flat — id moves to body / query |
POST /v1/triggers/{triggerEventId}/enable | Removed (404). Triggers are always on after creation; whether they fire is gated by automation.published. |
POST /v1/triggers/{triggerEventId}/disable | Removed (404). Unpublish the bound automation via PATCH /v1/automations { automationId, published: false }. |
GET/PATCH /v1/automations/{automationId} | flat — id moves to body / query |
POST /v1/automations/{automationId}/publish | PATCH /v1/automations { automationId, published: true } |
POST /v1/automations/{automationId}/test | POST /v1/automation/runs { automationId, mode: 'test' } |
GET /v1/automations/{automationId}/versions | GET /v1/automations?automationId=…&include=versions |
Deprecation
response header — see Response headers.
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.triggers.create,
brew.automations.create, brew.automationRuns.fire,
brew.emails.generate, brew.sends.create, …).
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 triggers → emails →
automations → fires.
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.