Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

216 Commits

Folders and files

Repository files navigation

@brew.new/sdk

Official TypeScript SDK for the Brew public API.

  • Resource-oriented surface — brew.contacts.upsert(...) instead of raw HTTP plumbing.
  • One typed error class (BrewApiError) for every non-2xx path, and typed transport errors (BrewTimeoutError, BrewConnectionError) when no answer arrives.
  • A deadline (timeoutMs) and cancellation (signal) that cover the whole request, response body included.
  • Safe retries with exponential backoff + jitter.
  • Auto-generated Idempotency-Key on POST so retries never double-write.
  • Node 20+ for the server API client. API keys stay on your backend; browser canvas integrations use the separate @brew.new/sdk/react or /embed entry.

Install

bun add @brew.new/sdk
# or
npm install @brew.new/sdk

Quick start

import { createBrewClient } from '@brew.new/sdk'

const brew = createBrewClient({
  apiKey: process.env.BREW_API_KEY!,
})

const contact = await brew.contacts.upsert({
  email: 'jane@example.com',
  firstName: 'Jane',
  customFields: { plan: 'enterprise' },
})

const found = await brew.contacts.get('jane@example.com')

const { data } = await brew.contacts.search({
  filters: [
    { field: 'customFields.plan', operator: 'equals', value: 'enterprise' },
  ],
})

That's the whole shape. Every other method follows the same pattern: list(query) for a page, get(id) for the bare row, and a 404 — not an empty page — when the id is unknown.

Upgrading from 10.x? 11.0.0 turns emails.previewClients into a rendering job you poll with emails.getClientPreview, and drops the deprecated verificationStatus mirror on contacts. From 9.x? 10.0.0 tracks the public API v1 cleanup. Read the CHANGELOG for both migrations.

Images: upload, add, delete

The brand image library is what the email agent searches when it designs. Adding and deleting images is free.

import { openAsBlob } from 'node:fs'

// A local file, in one call: opens an upload, sends the bytes, adds it.
const logo = await brew.content.uploadImage({
  file: await openAsBlob('./assets/logo.png'), // or a Buffer / ArrayBuffer
  fileName: 'logo.png', // contentType inferred from the extension
})
// → { url: 'https://cdn.brew.new/…', width, height, aspectRatio, assetId }

// A public URL (or up to 100 with `imageUrls`, imported in the background).
const hero = await brew.content.addImage({
  imageUrl: 'https://acme.com/hero.jpg',
})

// Remove one from the library. Emails already using it keep rendering.
await brew.brand.deleteImage(hero.assetId) // → { assetId, deleted: true }

uploadImage takes a Blob/File, an ArrayBuffer or a Uint8Array up to 20 MB (2 MB for SVG) as PNG, JPEG, GIF, WebP, AVIF, TIFF or SVG. Pass contentType when the file name has no such extension. An empty file, or one over those limits, throws a TypeError before any request. The bytes go straight to the upload's own URL without your API key, and that step is retried like any other (a repeat never replaces the first file). Opening the upload is not retried by default, because the API never replays it and each retry would hold one of the brand's 20 upload slots for 15 minutes; if it fails, call uploadImage again. To drive the steps yourself, call brew.content.createImageUpload({ fileName, contentType, size }), POST the raw bytes to its uploadUrl within 15 minutes, then brew.content.addImage({ uploadId }).

If the final addImage answer is lost, its transport error exposes uploadId and idempotencyKey. Recover with brew.content.addImage({ uploadId }, { idempotencyKey }) on the same brand client. Calling uploadImage again opens a new upload, so reusing the original key there causes IDEMPOTENCY_CONFLICT. See Recovering an image upload for a typed recovery example and the replay limits.

These recovery fields are part of the next SDK release. SDK 13.3.0 callers must use the explicit steps above and retain the upload ID themselves.

Pointing at a different environment

baseUrl is configurable. By default it points at production (https://brew.new/api), but you can override it for staging, local development, or a custom proxy:

const brew = createBrewClient({
  apiKey: process.env.BREW_API_KEY!,
  baseUrl: process.env.BREW_API_URL ?? 'https://brew.new/api',
})

Common values:

  • https://brew.new/api — production (default)
  • https://staging.brew.new/api — staging
  • http://localhost:3000/api — your own dev server

Trailing slashes are normalized either way. See docs/configuration.md for the full list of config options (timeoutMs, maxRetries, retryOnTimeout, signal, userAgent, custom fetch, etc.).

Documentation

Embed a group or single email in your app with EmailCanvas, the React component from @brew.new/sdk/react, using a publishable clientId and exactly one groupId or emailId. In SDK 13.2+, pass brandId to reuse an organization-wide client ID across current and future brands; client apps can also be restricted to one brand. No Clerk keys or browser API keys are required. A plain JavaScript iframe controller is available from @brew.new/sdk/embed.

Topic File
Analytics resource docs/analytics.md
Automations resource docs/automations.md
Audiences resource docs/audiences.md
Brand resource docs/brand.md
Brands (lifecycle) docs/brands.md
Chats resource docs/chats.md
Client configuration docs/configuration.md
Contacts resource docs/contacts.md
Domains resource docs/domains.md
Emails resource (+ send) docs/emails.md
Fields resource docs/fields.md
Insights resource docs/insights.md
Notifications resource docs/notifications.md
Error handling docs/errors.md
Retries + idempotency docs/retries-and-idempotency.md
Sends resource (reads + lifecycle) docs/sends.md
Templates resource docs/templates.md
Flows resource docs/flows.md
Development + OpenAPI sync docs/development.md
Releasing a new version RELEASING.md

Note: brew.brand.* (singular) reads the brand the CURRENT request acts on — its design system, identity, and extraction readiness. brew.brands.* (plural) is the organization-level lifecycle: list, create, and poll brands.

An API key is scoped either to ONE BRAND or to the whole ORGANIZATION. A brand-scoped key resolves its brand automatically. An organization-scoped key must name one per request — pass brandId in the client config, or pin one with client.withBrand(id). There is no default brand: omitting it returns 400 BRAND_ID_REQUIRED.

const brew = createBrewClient({ apiKey: process.env.BREW_API_KEY! })
const { data: brands } = await brew.brands.list()
const acme = brew.withBrand(brands[0].brandId)
await acme.emails.list()

Development

Use a modern Node 20 or newer runtime for SDK development. On this machine, Vitest worked with Node 20.19.2 and newer, but failed on the older system Node 20.10.0.

bun install
bun tsc         # typecheck
bun lint        # eslint
bun run format  # prettier
bun run test    # vitest — NOT `bun test`, that hits Bun's built-in runner and bypasses MSW setup
bun run build   # tsup: dist/ with esm + cjs + .d.ts

Full contribution + testing conventions live in AGENTS.md. The big one: every change is driven red → green via vitest + MSW, no exceptions for "trivial" pure functions.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages