---
title: Authentication
description: "API key authentication, tier system, rate limits, key rotation, and creating an account via Stripe checkout."
---

> **For AI agents:** the complete documentation index is at [llms.txt](https://docs.boringmarketing.com/llms.txt). Append `.md` to any page URL for its markdown version.

The API uses a single API key for authentication. No sessions, no OAuth, no passwords. One key, one header.

## API key format

All keys start with `bm_` followed by a hex string:

```
bm_54249af28748e0e24b4d2782122e2de9335627ef0f800747
```

Pass it in the `X-API-Key` header on every request:

```bash
curl -H "X-API-Key: $BM_API_KEY" \
  https://api.boringmarketing.com/auth/me
```

<Tip>
Store your key in an environment variable. Never hardcode it in source files.

```bash
export BM_API_KEY="bm_your_key_here"
```
</Tip>

## Getting an API key

Accounts are created via Stripe checkout. You choose a tier, pay, then retrieve your key from the dashboard. There is no free tier and no self-serve `/auth/register` endpoint.

```bash
curl -s -X POST \
  https://api.boringmarketing.com/auth/checkout \
  -H "Content-Type: application/json" \
  -d '{
    "email": "you@example.com",
    "name": "Your Name",
    "tier": "solo"
  }'
```

Response:

```json
{
  "status": "email_sent",
  "session_id": "bmc_k7f3...",
  "poll_url": "https://api.boringmarketing.com/auth/checkout/bmc_k7f3.../status",
  "sent_at": "2026-04-08T18:42:11.123Z"
}
```

<Note>
**Path split.** The next steps diverge based on who's reading.

**If you're a human:** check your email for the Stripe link, click it, complete payment. Then poll `poll_url` until it returns `{status: "completed", dashboard_url}`, open that URL, sign in with a magic-link, and copy your key from **Settings → API Access**.

**If you're an agent running unattended:** surface `session_id` and `poll_url` to your operator and stop. Polling will return `completed` after payment, but the response never contains an API key — the operator must sign into the dashboard and paste the key back before you can continue. To skip this handoff when you already hold a key (e.g. tier upgrade), use the [authenticated fast path on `POST /auth/checkout`](https://docs.boringmarketing.com/api-reference/auth/checkout).
</Note>

**Check your inbox.** The server emailed you a Stripe checkout link — click it, complete payment, then poll `poll_url` until status is `completed`:

```json
{
  "status": "completed",
  "dashboard_url": "https://dashboard.boringmarketing.com"
}
```

**The poll endpoint does not return your API key.** Open `dashboard_url` in a browser, request a magic-link with your email, and copy your key from **Settings → API Access**. Then export it:

```bash
export BM_API_KEY="bm_your_key_here"
```

<Tip>
  **Already have a key but need to upgrade?** The same `POST /auth/checkout` endpoint accepts an `X-API-Key` header for an authenticated fast path. In that mode, the response includes `checkout_url` inline (no email) and opens the Stripe billing portal for active users. See the [checkout endpoint reference](https://docs.boringmarketing.com/api-reference/auth/checkout) for details.
</Tip>

## Tiers

| | Builder | Pro | Agency |
|---|---|---|---|
| **Price** | $49/mo | $149/mo | $399/mo |
| **Calls/day** | 500 | 5,000 | 50,000 |
| **Brands** | 1 | 3 | 10 |

All paid tiers include the full pipeline, brief generation, and content drafting — the only differences are daily call volume and concurrent brands. Start with Builder to run a full pipeline on one brand; upgrade to Pro or Agency when you need more brands or higher daily call volume.

## Rate limits

Rate limits protect infrastructure. They are separate from tier call limits.

| Endpoint | Limit |
|----------|-------|
| `POST /auth/checkout` (anonymous) | 3 per hour per destination email + 5 per hour per IP |
| `POST /auth/checkout` (authenticated) | 5 per hour per IP |
| `POST /auth/recover` | 3 per hour per IP |
| All authenticated endpoints | Tier-based daily call limit |

When rate limited, the response body includes a `retry_after` field (seconds) and the response carries a `Retry-After` HTTP header with the same value:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
```

```json
{
  "error": "rate_limit_exceeded",
  "message": "Too many checkout requests. Try again later.",
  "retry_after": 60
}
```

## Error handling

| Status | Error Code | Meaning | What to Do |
|--------|-----------|---------|------------|
| `401` | `authentication_required` | Missing `X-API-Key` header | Set the header on your request |
| `401` | `invalid_api_key` | Header present but key is not recognized | Verify the key or rotate via `POST /auth/rotate-key` |
| `402` | `credits_exhausted` | Daily call limit reached for your tier | Upgrade via `POST /auth/billing-portal` |
| `404` | — | Resource not found | Verify the brand_id or resource ID |
| `409` | — | Pipeline run already in progress | Poll the existing run instead (check `X-Running-Run-Id` header) |
| `422` | — | Validation error | Check request body against the docs |
| `429` | — | Rate limited | Wait and retry after `Retry-After` seconds |

Error responses come in two flat shapes — both at the top level of the JSON body, with no `detail` wrapper:

```json
// Structured (most authenticated endpoints — auth gating, tier limits, rate limits)
{
  "error": "error_code",
  "message": "Human-readable explanation",
  "actions": [
    {
      "rel": "checkout",
      "href": "/auth/checkout",
      "method": "POST",
      "description": "Start Stripe checkout to create an account"
    }
  ]
}
```

```json
// Plain (some endpoints raise HTTPException with a string detail — e.g. 404 brand not found, 403 brand-limit-exceeded)
{ "detail": "Brand not found." }
```

The `actions` array, when present, provides machine-readable next steps — useful for AI agents to self-recover. Not every error includes one; agents should treat its absence as "no automated recovery available, escalate to a human."

### HATEOAS actions

Error responses include an `actions` array with machine-readable recovery steps. This is designed for AI agents:

```json
{
  "error": "credits_exhausted",
  "message": "Daily limit reached for your tier.",
  "usage": { "limit": 500, "used": 500 },
  "actions": [
    {
      "rel": "upgrade",
      "href": "/auth/billing-portal",
      "method": "POST",
      "description": "Open Stripe billing portal to change plan"
    }
  ]
}
```

An agent can read the `actions` array and automatically offer the user an upgrade path.

## Key rotation

Rotate your key without downtime. The old key is invalidated immediately when the new one is issued:

```bash
curl -s -X POST \
  -H "X-API-Key: $BM_API_KEY" \
  https://api.boringmarketing.com/auth/rotate-key
```

```json
{
  "api_key": "bm_new_key_here",
  "message": "API key rotated. Old key is now invalid."
}
```

Update your environment variable immediately after rotation.

## Upgrading your plan

Change plans via the Stripe billing portal. Open the portal URL in a browser to manage subscription, payment method, or invoices:

```bash
curl -s -X POST \
  -H "X-API-Key: $BM_API_KEY" \
  https://api.boringmarketing.com/auth/billing-portal
```

## Lost your API key?

If you lose your key, sign into the [dashboard](https://dashboard.boringmarketing.com) with your email to view or rotate it from the Settings page. The dashboard uses a magic-link login — no password required.

<Note>
**Path split.** Recovery always terminates at a browser sign-in. **Humans:** follow the link above. **Agents running unattended:** you cannot self-recover — surface `https://dashboard.boringmarketing.com` to your operator. Prefer storing the key durably at issue time rather than recovering it later.
</Note>
