Start Checkout
Create an account via Stripe checkout. Two modes: anonymous (server emails the link) or authenticated (fast-path inline URL).
Initiates a Stripe checkout session to create a new account, reactivate a cancelled one, or open the billing portal for an active one. This is the only self-serve onboarding path — POST /auth/register returns 410 Gone.
The endpoint has two modes selected by the presence of X-API-Key:
| Mode | Trigger | Behavior | Response |
|---|---|---|---|
| Anonymous | No X-API-Key header | Server creates the Stripe session and emails the URL to body.email. No URL is returned in the HTTP response. | {status: "email_sent", session_id, poll_url, sent_at} |
| Authenticated | X-API-Key header AND the key-owner's email matches body.email | Agent-native fast path. URL is returned inline, no email sent. | {checkout_url, session_id, poll_url, flow} |
Anonymous mode response is intentionally byte-identical across all account states. Whether body.email is a new user, an active subscriber, or a cancelled one, the shape and status are the same. This closes a response-shape enumeration oracle. Do not try to infer account state from the response — use GET /auth/me after the user has their key to check state.
Request body
emailstringrequiredEmail address. In anonymous mode this is where the Stripe checkout link is delivered. In authenticated mode it must match the X-API-Key owner's email exactly.
namestringDisplay name for the account. Optional.
tierstringrequiredTarget tier: builder, pro, or agency.
Anonymous mode (recommended for agents)
The typical agent signup flow. No pre-existing credentials are needed.
Request
curl -s -X POST \
https://api.boringmarketing.com/auth/checkout \
-H "Content-Type: application/json" \
-d '{
"email": "dev@example.com",
"name": "Dev User",
"tier": "builder"
}'
Response (always this shape)
{
"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"
}
The server sends the Stripe URL to email. Tell the user to check their inbox, click the link, and complete payment. Then poll poll_url (see Checkout Status) until it returns completed.
How the user retrieves their API key
After payment completes, the poll endpoint returns {status: "completed", dashboard_url} — not an API key. Anonymous signups retrieve their key via the dashboard:
- User opens
dashboard_url. - User enters their email → receives a magic-link.
- User clicks the link → lands in the dashboard.
- User copies the key from Settings → API Access.
This indirection closes a security oracle that existed in an earlier version of the flow where the session_id alone was sufficient to claim a key.
Authenticated mode (fast path)
Use this mode when an agent already has an API key for the user and needs to upgrade a tier, reactivate a cancelled subscription, or open the billing portal.
The X-API-Key header must be present AND body.email must match the key-owner's email exactly.
Request
curl -s -X POST \
https://api.boringmarketing.com/auth/checkout \
-H "Content-Type: application/json" \
-H "X-API-Key: $BM_API_KEY" \
-d '{
"email": "you@example.com",
"name": "You",
"tier": "pro"
}'
Response
{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_live_...",
"session_id": "bmc_m2p8...",
"poll_url": "https://api.boringmarketing.com/auth/checkout/bmc_m2p8.../status",
"flow": "new"
}
The flow field is a UX hint:
flow | Meaning |
|---|---|
new | Fresh registration. Show "Welcome". |
reactivation | Existing cancelled user reactivating. Show "Welcome back". |
active | Already-active caller. The checkout_url is a billing portal URL for plan changes. |
Auth failures
If X-API-Key is valid but body.email doesn't match the key-owner, the endpoint returns 401 with a generic error — deliberately indistinguishable from an invalid key. This prevents account enumeration via the checkout surface.
Rate limits
Anonymous mode is protected by a two-layer limiter:
| Limiter | Limit | Scope |
|---|---|---|
| Per-email | 3 / hour | Redis-backed, hashed key |
| Per-IP | 5 / hour | slowapi |
A 429 response is identical whether the per-email or per-IP limit triggered — no oracle. If the user claims they didn't receive the first email, wait at least 60 seconds before retrying. A 4th POST within the hour will lock the address for the full window.
Authenticated mode is only bound by the per-IP limiter since the caller is already authenticated.
Error responses
| Status | Error | Meaning |
|---|---|---|
401 | authentication_required | Authenticated mode: API key invalid OR email mismatch |
429 | rate_limited | Per-email or per-IP limit exceeded |
503 | email_delivery_failed | Anonymous mode: Resend returned a non-2xx. The session was not persisted; retry. |
Prefer a browser flow? Point humans at dashboard.boringmarketing.com — same Stripe checkout with a UI, and after sign-in the API key is visible on the Settings page.