Skip to main content

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 / split nodes (/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.
Brand management lives in the Brew dashboard — the public API does not expose endpoints to create, list, or edit brands. Each API key is bound to exactly one brand at creation time and can never be retargeted later. A brand can have any number of keys (dev, staging, production, per-service, per-teammate); each one acts on the same single brand it was created against. Need a key for a different brand? Switch brands in the dashboard and create a new key. Create and manage your API keys at brew.new/settings/api. The full endpoint reference in this docs site is generated from Brew’s OpenAPI source of truth. That means the human-written overview on this page explains the flow. The generated endpoint pages explain the exact request and response contract.

Base URL

https://brew.new/api
All current public endpoints are under the /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:
Authorization: Bearer brew_your_api_key
X-API-Key: brew_your_api_key
Keep API keys on your server only. Do not put them in browser code.

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 a brandId 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.
ScopeEndpointsBehavior
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-wideGET /v1/templatesReturns the public template catalog.
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. 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 to all. Routes require either the listed scope or all:
PermissionEndpoints
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)
allevery endpoint above
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.

How The API Fits Together

The flat surface is built around two delivery modes:
  1. Event-driven automations — define a trigger, mint the email bodies, assemble an automation graph that wires the trigger to one or more sendEmail nodes, publish it, and fire POST /v1/automation/runs from your backend whenever the real event happens. The workflow runtime delivers per-recipient.
  2. One-shot audience campaigns — generate (or pick) an email, then POST /v1/sends with a brand-owned audienceId. The workflow runtime fans out to every audience member.
trigger (POST /v1/triggers)

   │  email bodies (POST /v1/emails { emailType: 'automation' | 'transactional' })
   │  domain      (GET /v1/domains)
   │  audience    (GET /v1/audiences  — for /v1/sends only)

automation graph (POST /v1/automations { nodes, connections })


publish (PATCH /v1/automations { published: true })

   ▼                        one-shot campaign send
fire (POST /v1/automation/runs)         (POST /v1/sends { emailId, audienceId })
   │                                                 │
   ▼                                                 ▼
runs + logs (GET /v1/automation/runs ?include=logs)  workflow run (status, runId)

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 } (no provider / 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 } returns 400 INVALID_REQUEST.
  • DELETE /v1/triggers — delete (refused with 409 TRIGGER_HAS_DEPENDENT_AUTOMATIONS if non-archived automations still reference it).

Emails — /v1/emails

AI-generated email bodies. Every email row carries an emailType:
emailTypeWhere it showsUsed by
campaign/emails canvas (default board)POST /v1/sends.
automationHidden from /emails canvassendEmail 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 by emailType.
  • POST /v1/emails — generate (required body { prompt, emailType, contentUrl?, referenceEmailId? }). Response is a GeneratedEmailArtifact (with emailId + emailVersionId) or a TextResponse (the agent answered with prose).
  • PATCH /v1/emails — edit an existing email. Optional emailVersionId pins 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: true validates without writing.
  • GET /v1/automations — always returns { automations: AutomationRow[] }. ?include=versions (single-row mode) attaches versions[] inline on the row.
  • PATCH /v1/automations — update OR publish (published: boolean). Publishing runs validateAutomationForPublish and returns 409 PUBLISH_VALIDATION_FAILED with details.blockers[] when not ready.
  • DELETE /v1/automations — cascade.
Every 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 carries details.automationRunIds[].
  • Test{ automationId, mode: 'test', payload? } runs the automation in test mode (no real mail).
  • Replay{ automationId, triggerInstanceId, mode: 'replay' } (P7 — currently 501 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.
{
  emailId: string,
  emailVersionId?: string,     // pin to a specific version (defaults to latest)
  domainId: string,
  subject: string,
  previewText?: string,
  replyTo?: string,
  audienceId: string,          // REQUIRED
  scheduledAt?: string,        // ISO-8601, future only
}

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:
Deprecation: true
Sunset:      2026-12-01T00:00:00Z
Link:        </api/v1/automation/runs>; rel="successor-version"
Migrate to /v1/automation/runs before the sunset. See Legacy aliases below for the full per-route matrix.

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 — switch brands first if you want a key for a different brand.
2

Check that auth works

Use a simple read endpoint like domains:
curl -H "Authorization: Bearer brew_your_api_key" \
  https://brew.new/api/v1/domains
3

Generate an email

Create a saved email with a prompt + emailType (required):
curl -X POST "https://brew.new/api/v1/emails" \
  -H "Authorization: Bearer brew_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: email-generate-001" \
  -d '{
    "prompt": "Create a welcome email for new subscribers",
    "emailType": "campaign"
  }'
4

Send to an audience

Use the returned emailId, a verified domainId, and a saved audienceId:
curl -X POST "https://brew.new/api/v1/sends" \
  -H "Authorization: Bearer brew_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: send-001" \
  -d '{
    "emailId": "email_123",
    "domainId": "domain_123",
    "audienceId": "audience_123",
    "subject": "Welcome to Brew"
  }'
5

(or) Fire an automation

Build an automation graph against a published trigger, then fire it from your backend whenever the event happens:
curl -X POST "https://brew.new/api/v1/automation/runs" \
  -H "Authorization: Bearer brew_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: signup-jane@example.com-1779292800" \
  -d '{
    "triggerEventId": "tri_user_signup",
    "payload": { "email": "jane@example.com", "firstName": "Jane" }
  }'

Current Public v1 Surface

MethodEndpointPurpose
POST/v1/triggersCreate a custom brew_api trigger (deterministic).
GET/v1/triggersList every trigger in the brand (always { triggers: [...] }).
PATCH/v1/triggersUpdate trigger metadata (title, description, payloadSchema). Triggers don’t have a status field.
DELETE/v1/triggersDelete a trigger (refused if dependent automations exist).
POST/v1/emailsGenerate a new email (required emailType).
GET/v1/emailsList emails, filterable by emailType + status + timestamps.
PATCH/v1/emailsEdit an email. Optional emailVersionId pin.
POST/v1/automationsCreate an automation graph. dryRun: true validates only.
GET/v1/automationsList automations (always { automations: [...] }). ?include=versions on single-row.
PATCH/v1/automationsUpdate graph OR publish/unpublish.
DELETE/v1/automationsCascade-delete automation + versions + runs + logs.
POST/v1/automation/runsFire / test / replay (body-discriminated).
GET/v1/automation/runsList runs (always { runs: [...] }). ?include=logs attaches logs[].
PATCH/v1/automation/runsCancel an in-flight run (P7 — 501 today).
POST/v1/sendsStart a campaign send to a brand-owned audience.
GET/v1/contactsList / count / lookup contacts.
POST/v1/contactsUpsert one or many contacts.
PATCH/v1/contactsPatch a contact.
DELETE/v1/contactsDelete one or many contacts.
GET/v1/fieldsList custom contact field definitions.
POST/v1/fieldsCreate a contact field.
DELETE/v1/fieldsDelete a contact field.
GET/v1/audiencesList saved audiences.
GET/v1/domainsList verified sending domains.
GET/v1/templatesList public templates.
Legacy aliases (sunset 2026-12-01) — /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:
Deprecation: true
Sunset:      2026-12-01T00:00:00Z
Link:        <successor-route>; rel="successor-version"
Migrate before the Sunset timestamp to drop the deprecation noise. Wire shape is identical between alias and successor; the only difference is the response headers.
Legacy routeCanonical successor
POST/GET/PATCH /v1/executionsPOST/GET/PATCH /v1/automation/runs
POST /v1/eventsPOST /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}/enableRemoved (404). Triggers are always on after creation; whether they fire is gated by automation.published.
POST /v1/triggers/{triggerEventId}/disableRemoved (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}/publishPATCH /v1/automations { automationId, published: true }
POST /v1/automations/{automationId}/testPOST /v1/automation/runs { automationId, mode: 'test' }
GET /v1/automations/{automationId}/versionsGET /v1/automations?automationId=…&include=versions
Detect deprecation programmatically by reading the 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.

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:

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.