Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.brew.new/llms.txt

Use this file to discover all available pages before exploring further.

Brew’s two delivery primitives — campaign sends and automation runs — are asynchronous by design. The HTTP call accepts the work, persists an audit row, and returns a job identifier; the actual delivery happens on a durable Vercel Workflow runtime that retries, backs off, and emits per-step logs. This page covers the three async primitives, the canonical poll loop, and the agent-friendly recipes for concurrency.
Outbound webhooks for run / send / email-event lifecycle are on the roadmap. Until they ship, polling is the supported pattern. Both surfaces work today.

The three async primitives

PrimitiveEndpointResponsePoll with
Fire an automationPOST /v1/automation/runs (fire branch)202 { details: { automationRunIds: [...] } }GET /v1/automation/runs?automationRunId=…&include=logs
Test an automationPOST /v1/automation/runs { mode: 'test' }202 { automationRunIds: [...], status: 'test_started' }Same as fire
Send a campaignPOST /v1/sends202 { status: 'queued' | 'scheduled', runId, scheduledAt? }Resend / send-job analytics in the dashboard today; webhook coming
A POST /v1/emails (AI generation) is also long-running (~30–90s) but blocks the HTTP call until the agent renders the JSX or refuses. It is not a job-returning async — you get the artifact back in the response.

Fire a trigger, then poll the run

Fire returns a list of automationRunIds (one per matched published automation). Each id is a workflow run you can inspect.
import { setTimeout as sleep } from 'node:timers/promises'

async function fireAndPoll() {
  // 1. Fire — returns 202 with the run ids
  const fire = await fetch('https://brew.new/api/v1/automation/runs', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.BREW_API_KEY!}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `signup-${userId}-${eventTimestamp}`,
    },
    body: JSON.stringify({
      triggerEventId: 'tri_signup',
      payload: { email: 'jane@example.com', firstName: 'Jane' },
    }),
  })
  const fireBody = await fire.json() as {
    details: { automationRunIds: string[]; triggerInstanceId?: string }
  }
  const [runId] = fireBody.details.automationRunIds

  // 2. Poll the run until it terminates
  for (let attempt = 0; attempt < 60; attempt += 1) {
    const res = await fetch(
      `https://brew.new/api/v1/automation/runs?automationRunId=${runId}&include=logs`,
      { headers: { Authorization: `Bearer ${process.env.BREW_API_KEY!}` } }
    )
    const body = await res.json() as {
      runs: Array<{
        automationRunId: string
        status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
      }>
      logs?: Array<{ nodeName: string; status: string; durationMs?: number }>
    }
    const run = body.runs[0]!
    if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {
      console.log(`Run ${run.automationRunId}${run.status}`)
      for (const log of body.logs ?? []) {
        console.log(`  ${log.nodeName}: ${log.status} (${log.durationMs ?? '?'}ms)`)
      }
      return run
    }
    await sleep(2_000) // poll every 2 seconds
  }
  throw new Error('Run did not terminate within 2 minutes')
}
Latency expectationCadenceCap
Welcome / transactional flow (single sendEmail node)2s × 602 min
Multi-step drip with wait nodes (days)Don’t poll — read the run when you actually need the answer (e.g. nightly cron)n/a
QA / test fire1s × 3030s
The automation.runs.read policy is 100/min per key. A 2-second poll is well under that for a handful of concurrent agents.

Queue a campaign send, then verify

POST /v1/sends returns 202 { status, runId, scheduledAt? }. The runId is the Vercel Workflow run id (correlated with delivery analytics on the dashboard).
const send = await brew.sends.create({
  emailId: 'eml_promo',
  emailVersionId: 'emv_promo_v2',
  domainId: 'dom_brand_primary',
  audienceId: 'aud_subscribers',
  subject: 'Black Friday — 40% off',
})

console.log(send)
// { status: 'queued', runId: 'wrun_…' }
Per-recipient delivery status (delivered / opened / clicked / bounced) is recorded in Brew analytics today; the public read-API is roadmapped and tracked alongside outbound webhooks. For one-shot validations, the dashboard at brew.new/analytics is the source of truth.

Idempotency on async retries

Because retries are guaranteed-safe with idempotency keys, your agent / cron loop can re-fire the same operation without double-delivery. Same key + same body → same automationRunIds[], same runId, same response payload. See Idempotency for the full contract.

Agent concurrency patterns

Pattern 1 — fan out, fan in

An agent that runs 50 different welcome flows in parallel:
const targets = await loadNewSignups()         // 50 users

const fires = await Promise.all(
  targets.map((user) =>
    brew.automationRuns.fire(
      {
        triggerEventId: 'tri_signup',
        payload: { email: user.email, firstName: user.firstName },
      },
      { idempotencyKey: `signup-${user.id}-${user.signedUpAt}` }
    )
  )
)

// Each fire returns its own automationRunIds[]; collect for later inspection
const allRunIds = fires.flatMap((f) => f.automationRunIds)
The automation.runs.write policy is 60/min. For 50 parallel fires, you’re well under; for 200+ you’d batch with Promise.allSettled + a 429-aware retry helper.

Pattern 2 — long-poll a single run to terminal

Use a generator so the agent can yield other work between polls:
async function* watchRun(runId: string) {
  while (true) {
    const { runs } = await brew.automationRuns.get({
      automationRunId: runId,
      include: ['logs'],
    })
    yield runs[0]!
    if (['completed', 'failed', 'cancelled'].includes(runs[0]!.status)) return
    await new Promise((r) => setTimeout(r, 2_000))
  }
}

for await (const snapshot of watchRun(runId)) {
  await mySideEffect(snapshot)   // e.g. update a UI badge
}

Pattern 3 — bulk inspect by automation

For dashboards / agents that want “the last 100 runs of this automation”:
const { runs } = await brew.automationRuns.list({
  automationId: 'auto_abc',
  limit: 100,
})
const failed = runs.filter((r) => r.status === 'failed')

Why no webhooks today (and what’s next)

The fastest path to an A-grade Events & Streaming score is outbound webhooks; we’re shipping them. See the Webhooks & events page for the planned contract (webhook.run.completed, webhook.send.delivered, webhook.send.opened, …) and the roadmap. Until then, the poll loop on /v1/automation/runs is the supported way to wait for terminal status.

Endpoints that are already async-friendly

EndpointJob indicatorStatus read
POST /v1/automation/runs (fire)details.automationRunIds[] (one per matched automation)GET /v1/automation/runs?automationRunId=…
POST /v1/automation/runs { mode: 'test' }automationRunIds[]Same
POST /v1/sendsrunId (Vercel Workflow run id)Analytics on the dashboard today; public read API roadmapped
POST /v1/contacts (batch)summary.{inserted,updated,failed} — synchronousn/a

See also

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.