Developers

Ads by Breakfast API

Send a product or service page URL. Get 6 finished image ads with your real logo, your colors and copy from your own page. Async, credit based, versioned at /api/v1.

Quickstart

  1. Sign in at /account and create a test key. Test keys are free, never queue, and return 6 sample ads after about 20 seconds.
  2. Create a pack:
curl https://adsbybreakfast.com/api/v1/packs \
  -H "Authorization: Bearer $ABB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: client-a-2026-10-05" \
  -d '{
    "url": "https://shop.example.com/products/cold-brew",
    "format": "reels",
    "webhook_url": "https://hooks.your-agency.com/abb",
    "metadata": { "client": "client-a" }
  }'
HTTP/1.1 202 Accepted
{
  "id": "pk_3f9c0d1e2a4b4c6d8e0f1a2b3c4d5e6f",
  "object": "pack",
  "status": "queued",
  "credits_reserved": 6,
  "eta": "2026-10-06T12:00:00.000Z",
  "queue_position": 2,
  "links": { "self": "/api/v1/packs/pk_3f9c0d1e2a4b4c6d8e0f1a2b3c4d5e6f" }
}
  1. Wait for the pack.ready webhook, or poll:
curl https://adsbybreakfast.com/api/v1/packs/pk_3f9c0d1e2a4b4c6d8e0f1a2b3c4d5e6f \
  -H "Authorization: Bearer $ABB_API_KEY"
{
  "id": "pk_3f9c0d1e2a4b4c6d8e0f1a2b3c4d5e6f",
  "object": "pack",
  "status": "ready",
  "format": "reels",
  "url": "https://shop.example.com/products/cold-brew",
  "created_at": "2026-10-05T23:10:04.000Z",
  "eta": "2026-10-06T12:00:00.000Z",
  "ready_at": "2026-10-06T08:41:57.000Z",
  "metadata": { "client": "client-a" },
  "ads": [
    {
      "index": 1,
      "version": 1,
      "image_url": "https://adsbybreakfast.com/api/v1/assets/packs/.../01.v1.png?exp=...&sig=...",
      "width": 768,
      "height": 1344,
      "headline": "Cold brew that tastes like the cafe",
      "subline": "Steeped 18 hours",
      "cta": "Shop now",
      "rebakes_used": 0,
      "rebakes_left": 2,
      "rebake_in_progress": false
    }
  ],
  "zip_url": "https://adsbybreakfast.com/api/v1/assets/packs/.../pack.zip?exp=...&sig=...",
  "urls_expire_at": "2026-10-13T08:45:00.000Z"
}

The same flow in JavaScript (Node 18+):

const BASE = "https://adsbybreakfast.com/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.ABB_API_KEY}`,
  "Content-Type": "application/json",
};

// 1. Create the pack (safe to retry with the same Idempotency-Key)
const res = await fetch(`${BASE}/packs`, {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": "client-a-cold-brew-1" },
  body: JSON.stringify({ url: "https://shop.example.com/products/cold-brew", format: "feed" }),
});
const pack = await res.json();
if (!res.ok) throw new Error(`${pack.error.code}: ${pack.error.message}`);

// 2. Poll (or use a webhook). Live packs are ready by 7am CT.
let p = pack;
while (!["ready", "failed"].includes(p.status)) {
  await new Promise((r) => setTimeout(r, 10 * 60 * 1000));
  p = await (await fetch(`${BASE}/packs/${pack.id}`, { headers })).json();
}

// 3. Use the results
for (const ad of p.ads) console.log(ad.index, ad.headline, ad.image_url);

Auth

Send Authorization: Bearer abb_live_... or abb_test_.... Keys belong to your account and share its credits. We store only a hash, so a key is shown once when you create it. Revoke a key at /account and it stops working on the next request.

Live keys see live packs, test keys see test packs. Up to 10 active keys per account, one per client or workflow works well.

Endpoints

Method and pathWhat it does
POST/api/v1/packsCreate a pack. Body: url (required, public https), format reels (9:16, 768x1344, default) or feed (3:2, 1536x1024), count (6 only), webhook_url, metadata (up to 20 flat keys). Optional Idempotency-Key header. Returns 202 and reserves 6 credits.
GET/api/v1/packs/{id}Status: queued, cooking, needs_review (a person is checking it before it ships), ready, failed. When ready: 6 ads with signed image_url, headline, subline, cta and re-bake counts, plus zip_url.
GET/api/v1/packsNewest first. limit (1 to 100, default 20) and starting_after (a pack id). Returns data and has_more.
POST/api/v1/packs/{id}/ads/{index}/rebakeNew version of one ad. Body: reason (scene, literal, copy, wrong_claim, layout, product_size, logo, colors, other) and an optional note up to 200 characters. 2 free per ad, within 30 days of ready.
GET/api/v1/rebakes/{id}Re-bake status: queued, cooking, applied (new image_url and version), escalated (a person is on it), failed.
GET/api/v1/accountCredits, credits reserved by packs in progress, and how many packs this key can still start tonight.

Re-bake an ad

curl https://adsbybreakfast.com/api/v1/packs/pk_3f9c.../ads/4/rebake \
  -H "Authorization: Bearer $ABB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "product_size", "note": "Product looks too small" }'

Image and ZIP URLs are signed and expire after 7 days (urls_expire_at). Fetch the pack again for fresh links, and copy files somewhere you own instead of hotlinking. Copy, logo and claims always come from the page you send, so a page with thin copy makes thin ads.

Webhooks

Pass webhook_url on create and we POST each event to it: pack.queued, pack.cooking, pack.needs_review, pack.ready, pack.failed, rebake.applied, rebake.escalated. data is the same object the GET endpoints return.

POST https://hooks.your-agency.com/abb
ABB-Signature: t=1791273600,v1=5f0c...e91a
ABB-Event-Id: evt_8a1f0c2d3e4b5a6978c1d2e3
ABB-Event-Type: pack.ready

{
  "id": "evt_8a1f0c2d3e4b5a6978c1d2e3",
  "object": "event",
  "type": "pack.ready",
  "created": 1791273600,
  "data": { "id": "pk_3f9c...", "object": "pack", "status": "ready", "ads": [ ... ], "zip_url": "..." }
}

Every request is signed with the webhook secret shown when you created the key: v1 is the hex HMAC-SHA256 of t + "." + raw body. Check it before trusting the body:

import crypto from "node:crypto";

// Use the raw request body, before any JSON parsing.
export function verifyAbbSignature(rawBody, header, secret) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || "");
  if (!m) return false;
  if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false; // 5 minute window
  const want = crypto.createHmac("sha256", secret).update(`${m[1]}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(want, "hex"), Buffer.from(m[2], "hex"));
}

Errors

Errors look like { "error": { "code": "insufficient_credits", "message": "..." } }. Branch on code, show message.

HTTPcodeMeaning
400invalid_body, invalid_format, invalid_count, invalid_webhook_url, invalid_metadata, invalid_idempotency_key, invalid_reason, invalid_cursorFix the request.
400unsupported_fieldA field v1 does not take (for example brief). Copy always comes from the page.
401unauthorizedMissing or revoked key.
402insufficient_creditsA pack needs 6 credits. The body has credits and need.
403missing_scope, rebake_cap_reached, pack_rebake_cap_reachedKey lacks a scope, or the ad used its 2 free re-bakes.
404pack_not_found, ad_not_found, rebake_not_foundWrong id, or it belongs to another account or mode.
409idempotency_conflictSame Idempotency-Key, different body.
409pack_not_ready, rebake_in_progress, pack_rebakes_in_progress, pack_recook_runningWait and retry.
410rebake_window_closedRe-bakes close 30 days after the pack was ready.
422url_not_publicurl must be a public https page.
429rate_limitedToo many requests. Honor Retry-After.
429capacity_full_tonight, key_nightly_limitTonight is full for everyone, or for this key. Retry after next_slot_at.
500internal_errorOur side. Retry with the same Idempotency-Key.

A failed pack returns its credits automatically. error.code on a failed pack is one of url_unreachable, brand_unidentifiable, not_enough_distinct_claims or pack_failed.

Limits and credits

n8n

Import this workflow (Workflows, Import from File). It runs: Webhook (URL in), Create pack, Wait, Get pack, Status, Pack out, Deliver by email.

  1. Add a Header Auth credential: name Authorization, value Bearer abb_test_.... Pick it in Create pack and Get pack.
  2. Set your email credential and sender in Deliver by email, or swap that node for Drive, Slack or your CRM.
  3. Activate it and POST {"url": "...", "format": "reels", "deliver_to": "[email protected]"} to its webhook URL.

The pack webhook goes to the execution's resume URL, so the workflow wakes when the pack is ready. If your n8n is not reachable from the internet, it also checks every 30 minutes, so it works either way. It reads the pack with your key on every wake and never trusts the webhook body.

MCP

Agents that speak MCP (Claude, ChatGPT, Cursor, VS Code) can use the API through our small MCP server. Tools: create_pack, get_pack, rebake_ad, list_packs, get_account. Ask us for the package, then run npm install in its folder and add it to your client:

{
  "mcpServers": {
    "ads-by-breakfast": {
      "command": "node",
      "args": ["/path/to/integrations/mcp-server/index.mjs"],
      "env": { "ABB_API_KEY": "abb_test_..." }
    }
  }
}

For HTTP instead of stdio: node index.mjs --http --port 8787 serves stateless Streamable HTTP at /mcp, and each request sends its own Authorization: Bearer abb_....

Versioning

Breaking changes get a new version path. New fields can appear in responses at any time, so ignore fields you do not know. Machine-readable spec: /api/v1/openapi.json.

API docs | Ads by Breakfast