Skip to main content

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:
  1. 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.
  2. 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.
  3. Analytics is read-only reporting derived from sends + their per-recipient events.
  4. Automations wire a trigger to a graph of sendEmail / wait / filter / split nodes that deliver per-recipient when the trigger fires.
Use it to:
  • 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.
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. The human-written overview on this page explains the flow; the generated endpoint pages explain the exact request and response contract.

Base URL

All current public endpoints are under the /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:
Keep API keys on your server only. Do not put them in browser code.
Two endpoints bootstrap a new key:
  • GET /v1/brand{ brand } is the brand the key is pinned to. Check ready before 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 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. 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 to all. The coarse scopes imply the granular ones: contactsaudiences; emailsdomains + 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:
  1. One-shot campaigns: generate (or pick) a design, then POST /v1/sends with either a brand-owned audienceId or an inline to list (≤ 50). The workflow runtime fans out to every recipient and mints one sendId you poll for status + stats.
  2. Event-driven automations: define a trigger, generate the design bodies, assemble an automation graph that wires the trigger to one or more sendEmail nodes, publish it, and fire POST /v1/automations/triggers/{triggerEventId}/fire from your backend whenever the real event happens. The runtime delivers per-recipient, one sendId per recipient.
Both modes write rows to the same 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? }. Returns 201 { emailId, emailVersionId, html, previewImage? }, or 200 { 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). Returns 201 { emailId, emailVersionId, html, previewImage? }. Usage-metered.
  • POST /v1/emails/figma: transpile a Figma frame into an editable design { figmaUrl, title?, format?: 'jsx' | 'html' }. The figmaUrl must include a node-id, i.e. the link to one frame. It requires the API-key brand’s connected Figma integration; credentials never travel in the request. Returns 201 { emailId, emailVersionId, title, format, content, warningCount, exportedNodeCount, previewImage? }. Deterministic, so no model runs and it is free.
  • GET /v1/emails: list designs (filter status, createdAtFrom/To, updatedAtFrom/To). Pass ?emailId= for one design + its rendered html; the row already carries previewImage. Add ?include=html,versions to 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.
There is no longer a get-one, versions, preview, or per-design sends endpoint: the design read carries 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 by test. A campaign send { emailId, emailVersionId?, domainId, subject, previewText?, replyTo?, (audienceId | to), scheduledAt? } provides exactly one of audienceId (a brand-owned saved audience) or to (an inline email or array of ≤ 50) and returns 202 { 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-canceled send returns 200); once the send is sending, sent, or failed it is 409 SEND_NOT_CANCELLABLE, and an unknown / cross-brand id is 404 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 (filter status, from, to), narrow to one design with ?emailId=, or fetch one send with ?sendId=; poll its lifecycle status + aggregated stats. Add ?include=events to inline the per-recipient event feed (sent / delivered / opened / clicked / bounced / complained / unsubscribed).
  • GET /v1/analytics/campaigns: lifetime KPIs per campaign send (one row per sendId).
  • GET /v1/analytics/automations: windowed per-automation performance + totals (default last 30 days). automations scope.
  • GET /v1/analytics/events: unified event feed across domains (filter recipientEmail, 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). automations scope.

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: true validates without writing.
  • GET /v1/automations: list (lean rows; graph omitted). Pass ?automationId= for one automation, and ?include=graph,versions to embed the full { nodes, connections } graph and its version history.
  • PATCH /v1/automations/{automationId}: update name / description / nodes / connections / triggerEventId, or flip the lifecycle with { published }. { published: true, automationVersionId? } validates the graph and goes live (409 PUBLISH_VALIDATION_FAILED if it fails); { published: false } stops matching fires (422 AUTOMATION_NOT_PUBLISHED if it was never published). published is 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.
Every 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 } (no provider / providerEventKey. Those are rejected).
  • GET /v1/automations/triggers: list every trigger; pass ?triggerEventId= for one trigger row.
  • PATCH /v1/automations/triggers/{triggerEventId}: update title / 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 with 409 TRIGGER_HAS_DEPENDENT_AUTOMATIONS while 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 from details.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 (filter automationId, triggerEventId, triggerInstanceId, recipientEmail, status, mode, from, to). Pass ?automationRunId= for one run, and ?include=logs to attach its per-node logs[].

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 emailId, a verified domainId, and a saved audienceId:
Then poll 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.

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.