API reference

Last updated

Ballast is cold email software with every feature on every plan. Connect unlimited mailboxes, send sequences at a pace your domains survive, and handle replies in one inbox, priced by the emails you send.

The REST API has 107 endpoints in 19 areas, the same operations as the dashboard. Each one is also a tool for your AI through MCP. The OpenAPI spec is at /api/v1/spec.json for Postman or code generators.

Authentication

Create a key in Settings, API keys. It belongs to one workspace and acts with the role of the person who created it, so a member's key can't do what only admins can. Send it as a bearer token. Each key gets 600 requests a minute.

curl https://ballast.to/api/v1/me \
  -H "Authorization: Bearer $BALLAST_API_KEY"

Errors

Errors come back as JSON with an HTTP status, a code and a message written for people. Invalid input lists each problem under data.issues. 401: missing or revoked key. 402: the workspace needs an active plan. 403: your role can't do this. 429: slow down.

Endpoints

Each area has its own page with every endpoint, its parameters, what it returns, who can call it and a curl example.

Webhooks

Add endpoints in Settings, Webhooks, or with POST /webhooks. Ballast POSTs JSON for each event you pick. Answer with any 2xx within 10 seconds. Anything else is retried 8 more times with growing gaps, for about two hours; redirects are not followed. Every delivery has an id shared by all your endpoints, so you can ignore one you've already handled.

EventWhen
lead.repliedA lead answered a campaign email (out-of-office and auto-replies excluded).
lead.labeledA reply got a label, from AI, a rule, a person or the API.
lead.bouncedAn email to a lead bounced hard.
lead.unsubscribedA lead unsubscribed, by link, one-click header or reply.
lead.wonA lead was marked won.
campaign.pausedA campaign stopped sending: paused by someone, a label, a breaker or its end date.

Payload

Lead events carry the lead and its campaign; the rest depends on the event: reply, label, bounce, value for a win, reason for a paused campaign.

lead.replied
{
  "id": "0199f2b4-6c1e-7a51-9d8e-3f4b2c1a0e77",
  "type": "lead.replied",
  "created_at": "2026-10-17T09:14:03.120Z",
  "workspace_id": "0199a1b2-...",
  "data": {
    "lead": {
      "id": "0199...",
      "email": "jane@example.com",
      "first_name": "Jane",
      "last_name": "Doe",
      "company": "Example Inc",
      "title": "Head of Sales",
      "status": "replied",
      "custom": {}
    },
    "campaign": {
      "id": "0199...",
      "name": "Q4 founders",
      "status": "running"
    },
    "reply": {
      "thread_id": "0199...",
      "mailbox": "you@yourdomain.com",
      "subject": "Re: Quick question",
      "text": "Sounds interesting, what does it cost?",
      "received_at": "2026-10-17T09:13:58.000Z"
    }
  }
}

Check the signature

Headers: Ballast-Event, Ballast-Delivery and Ballast-Signature: t=<unix seconds>,v1=<hex>, an HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the endpoint's secret (whsec_...). Refuse signatures older than 5 minutes.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// header = the Ballast-Signature request header, rawBody = the body exactly as received
export function verifyBallast(secret, rawBody, header) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}