Shipping the BoomSauce API
The full v1 API is live. The Node SDK is on npm. Four lines of TypeScript to send your first email.
```ts import { BoomSauce } from 'boomsauce'; const bs = new BoomSauce({ apiKey: process.env.BOOMSAUCE_API_KEY! }); const send = await bs.sends.create({ from_brand_id: 1, to: { email: 'jane@example.com', first_name: 'Jane' }, subject: 'Quick question, Jane', html: '<p>Hey Jane, mind if I send a 2-line pitch?</p>', }); ```
That returns a typed response with the send_id, the provider message_id, the mailbox used, and a request_id you can paste into a support ticket if anything goes wrong. The SDK handles 429s and 5xx with exponential backoff, stamps an Idempotency-Key on every write so retries are safe, and exposes every error as a typed class you can catch by kind. Webhook signature verification is one function: `verifyWebhook(rawBody, header, secret)`. Zero runtime dependencies. Works on Node 18+, Bun, Deno, Cloudflare Workers.
This post is a walk-through of what we built, the decisions behind it, and the things we deliberately didn't ship.
## Why an API at all
The BoomSauce dashboard is the right product for ninety percent of operators. The remaining ten percent — RevOps engineers wiring email into a sales pipeline, lifecycle teams sending behavior-triggered messages, agencies that manage hundreds of campaigns programmatically — need to drive the rails from code. Without an API, they hand-roll a headless browser, scrape responses, paginate by guessing, and hate every minute of it.
We had a v1 surface for months, but it was a thin wrapper. No scopes. No IP allowlist. No call log. No SDK. Documentation was a hand-written page with curl snippets that drifted out of sync with the code. The API existed in the technical sense — you could call it — and didn't exist in the developer-experience sense, which is the only sense that matters.
This sprint closed the gap.
## What "pro grade" meant
We benchmarked against four operators that set the standard in their lanes — Stripe, Resend, Postmark, SendGrid — and built a punch list of every gap. The gaps were predictable: no first-party SDK, no per-key scopes, no IP allowlist, no audit log, no call log, no docs you'd want to send to a CTO. We built each one.
**Per-key scopes.** Every key now ships with a scope set: `sends:write`, `contacts:read`, `webhooks:write`, and so on, plus resource-level wildcards (`sends:*`) and a full-access `*` for trusted backends. Endpoints declare what they require. A 403 explains exactly which scope is missing and which scopes the key has, so the integrator's fix is one click in the dashboard, not a support ticket.
**IP allowlist.** Per-key CIDR list, enforced in the database via Postgres' native `<<=` containment check at auth time. NULL means any IP. Populate it with your CI runner's egress range and a key leaked into a Slack channel becomes useless to whoever finds it.
**Per-key rate limit.** Default is 300 requests per minute per tenant. Override per key when one automation needs more headroom and another needs to be capped tightly. Limits surface in `X-RateLimit-Limit / Remaining / Reset` headers on every response.
**Key rotation.** Click rotate. We issue a new key with the same scopes and limits. The old key keeps working for 24 hours so you can swap it across your services without downtime, then auto-revokes. Audit log captures the whole flow.
**Audit log + call log.** Every key lifecycle event — created, revoked, rotated, scopes changed, IP list changed — lands in `mt_api_key_events` with the actor's email and IP. Every authenticated request lands in `mt_api_calls` with the request_id, status, latency, IP, and idempotency-key. Both are visible in the dashboard. Both let support find the exact call when a customer pastes a request_id into a ticket.
**Idempotency replay inspector.** Send the same request twice with the same Idempotency-Key and the second response carries `X-Idempotent-Replay: true` and `X-Original-Request-Id: …` pointing back to the original execution. The call log tags replays distinctly so you can tell cached responses from real ones in your dashboard.
**Errors with docs_url.** Every 4xx and 5xx response includes `error`, `message`, `request_id`, and `docs_url`. The docs_url deep-links to a section in /api-docs where the error code is documented with status, what it means, and how to fix it. Less guessing. Faster integration.
## What the SDK does
The HTTP wrapper inside the SDK is the most important file. It owns:
- Bearer token injection on every request - Auto-stamping a UUID Idempotency-Key on POST/PATCH unless the caller set one - Exponential backoff on 429 and 5xx (honors Retry-After when set) - Timeout enforcement with proper AbortController integration - Pluggable fetch (so tests inject a mock and Cloudflare Workers can pass workerd's fetch)
Above that sits eight resource subclients — `bs.sends`, `bs.contacts`, `bs.webhooks`, etc. — each typed with the exact request and response shapes from the OpenAPI spec. Below it sits an error hierarchy:
```ts import { BoomSauce, RateLimitError, AuthError, ValidationError } from 'boomsauce';
try { await bs.sends.create({ /* ... */ }); } catch (err) { if (err instanceof RateLimitError) await sleep(err.retryAfterSeconds * 1000); else if (err instanceof AuthError) throw new Error('check BOOMSAUCE_API_KEY'); else if (err instanceof ValidationError) showFormError(err.message); else throw err; } ```
For webhooks, one function:
```ts import { verifyWebhook } from 'boomsauce';
export async function POST(req: Request) { const raw = await req.text(); try { const event = verifyWebhook( raw, req.headers.get('BoomSauce-Signature') ?? '', process.env.BOOMSAUCE_WEBHOOK_SECRET!, ); // do stuff with event.event and event.data return new Response(null, { status: 200 }); } catch { return new Response('invalid signature', { status: 400 }); } } ```
It's HMAC-SHA256, timing-safe compare, with a configurable freshness tolerance (default 5 minutes) to defeat replay attacks. The verifier returns a typed event payload on success and throws `SignatureError` on failure.
Twenty-two tests cover the lot — header stamping, retry on 429 with Retry-After, retry exhaustion, error class mapping, signature verify positive and negative, tampered body, expired timestamp.
## Docs that aren't a wall of text
The /api-docs page is now an embedded Scalar reference. Three-pane layout. Search. Try-it-now panel that uses your real API key once you paste it into the auth helper at the top. Four code samples per endpoint — curl, JavaScript fetch, Python requests, Go net/http — auto-generated from the OpenAPI spec on every deploy so they can't drift.
The Postman collection at /api/v1/postman.json is the part most operators don't bother with. Most "we have a Postman collection" stories are mechanical OpenAPI dumps with empty headers and `string` placeholders. Ours has realistic example bodies, a pre-request script that stamps Idempotency-Key on every write, a test script that asserts the response is 2xx and captures the request_id to a collection variable, per-endpoint descriptions with the required scope and likely error codes, and a companion environment file at /api/v1/postman.environment.json. Drop them both into Postman, set your api_key, and you're sending.
## What we deliberately didn't ship
We considered and skipped several things that look like polish but compound into complexity:
**GraphQL.** Adds a second surface area for zero gain at this scale. REST plus the SDK covers every use case we've seen.
**OAuth Connect.** "Apps build on BoomSauce" is a story for when a real partner asks for it. Speculating on it before there's demand burns engineering hours that should fix bugs in the existing surface.
**gRPC.** Same answer.
**Pagination styles other than cursor.** Limit/offset is a foot-gun at scale; opaque cursors are unambiguous. The SDK exposes them as opaque strings — you don't think about it.
**A CLI.** The Stripe CLI is excellent and webhook-listen is the most-loved feature. It's on the roadmap. Building it correctly takes a week and the SDK had to ship first.
**Scoped service tokens for impersonation.** Real need but speculative until enterprise customers ask. We have the audit log to add it cleanly when the moment comes.
## What's next
A short list, in order:
- A test mode that's actually useful — bs_test_* keys returning realistic mock IDs, never touching SMTP or wallet, so customers can run BoomSauce in CI without ceremony. - A webhook delivery UI redesign with a per-event timeline, replay button, and per-event-type pause toggle (currently all-or-nothing). - Webhook event catalog in the OpenAPI spec with full payload schemas. - A /status page rendered from the call log — real uptime per endpoint, public, no marketing fluff. - Versioning policy with Sunset and Deprecation headers per RFC 8594, so customers get six-month deprecation notices in-band when v2 lands.
A Python SDK and a Go SDK come after we have a few weeks of Node SDK in production telling us which patterns hold up and which need a rewrite.
## Try it
Generate a key at /developers, install the SDK, send your first email:
```bash npm install boomsauce ```
```ts import { BoomSauce } from 'boomsauce'; const bs = new BoomSauce({ apiKey: 'bs_live_...' }); console.log(await bs.wallet.retrieve()); ```
If you're integrating BoomSauce into a real system and hit a friction point, send a note to support@boomsauce.com with the request_id from the response. We read every one.
— Michael