{"openapi":"3.1.0","info":{"title":"CogniLead API","version":"v1","description":"CogniLead is a managed cold-email deliverability engine —\nwarmed sender-domain pools, a reputation circuit-breaker, bounce/reply handling,\nsuppression, and one-click unsubscribe, plus an opt-in paid lead-enrichment\nadd-on. You bring the leads (`POST /v1/leads`); it gets the mail into the inbox.\nEvery `/api/v1/*` route is tenant-scoped — your bearer key or\nSupabase SSR session pins the call to one tenant and every read /\nwrite goes through that frame.\n\nThis spec covers the public API surface as of v1. Auth is either a\nserver-side bearer key (`Authorization: Bearer pk_live_…`) minted\nat `POST /api/v1/keys`, or a Supabase SSR session cookie when calls\ncome from the dashboard. A few routes are session-only\n(`/tenants`, `/tenants/{id}/members`) — see each operation.\n\nCogniLead keeps the outbound hygiene it needs: suppression lists and\none-click (RFC 8058) unsubscribe.\n\nCross-cutting behaviour (applies to every operation, so it is not repeated\nper route): errors share one envelope `{ error, request_id, ... }`; a\ndeclared body over 1 MB gets `413 payload_too_large`; a malformed id gets\n`400 invalid_id`; a cookie-session write from another origin gets\n`403 cross_site_blocked` (API keys are unaffected); a session whose user has\nan enrolled second factor but has not passed it gets `403 mfa_required`. An\ninbound `X-Request-Id` is echoed only if it matches\n`^[A-Za-z0-9._:-]{1,128}$`; otherwise a fresh id is minted.\n\nLive docs: https://cognilead.ai/docs · Repository: see /docs/spec.","contact":{"email":"engineering@cognilead.ai"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://cognilead.ai","description":"production"}],"tags":[{"name":"api-keys","description":"Server-side API key lifecycle."},{"name":"audit","description":"Append-only audit trail of workspace mutations."},{"name":"billing","description":"Stripe Checkout, Customer Portal, subscription, and usage."},{"name":"campaigns","description":"Multi-step sequence CRUD and run triggers."},{"name":"customers","description":"End-customers of a reselling tenant — provisioning, scoped sub-keys, per-customer usage, credit balance, and top-up."},{"name":"dispatch","description":"Sequence-step dispatch entry points."},{"name":"domains","description":"Sending domains — search, quote and buy one (or connect your own), then follow it through DNS, reputation check, mailbox creation and warm-up."},{"name":"health","description":"Liveness and transport probes."},{"name":"leads","description":"Lead lifecycle — create, list, and intersect gating."},{"name":"live","description":"Live-updating dashboard snapshots (ETag/304 partial-poll channel)."},{"name":"media","description":"Image generation (PDF cover, marketing artefacts)."},{"name":"mta","description":"Operator-only: enrolment, provisioning and status of CogniLead-run MTA servers."},{"name":"pool","description":"Warmed sender pool — tenant mailbox requests, BYO warming and release, plus operator-only pool health, capacity, replenish and reputation feeds."},{"name":"reseller","description":"Reseller-level surfaces: fleet-wide consumption and the Monopea brain usage/billing/embed proxies."},{"name":"sends","description":"Per-lead dispatch history."},{"name":"status","description":"Operator platform health for unattended supervision."},{"name":"suppressions","description":"Email- and domain-level outreach blocks."},{"name":"tenants","description":"Tenant + membership management."},{"name":"unsubscribe","description":"Inbound mailto unsubscribe webhook."},{"name":"webhooks","description":"Outbound webhook endpoints — CogniLead POSTs signed delivery + reputation events to your URLs."}],"paths":{"/api/v1/health":{"get":{"operationId":"getHealth","tags":["health"],"summary":"Service health","description":"Liveness: running version, DB mode and whether the database answers (db_ok), plus the PHI Gateway mode. Public — no auth required, no tenant data. The detailed verdict is the operator-only /api/v1/internal/status.","security":[],"responses":{"200":{"description":"Health snapshot."},"500":{"description":"Internal error producing the health snapshot."}}}},"/api/v1/health/email":{"get":{"operationId":"getHealthEmail","tags":["health"],"summary":"Email transport health","description":"Reports the configured + active outbound-email mode (memory | http | smtp), unsubscribe base URL, and which provider environment variables are present (presence only — no values). Operator only: platform mail-infra configuration, not tenant data, so any other workspace gets 403.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Email transport snapshot."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Not the operator tenant (`forbidden`, \"operator tenant only\")."}}}},"/api/v1/leads":{"get":{"operationId":"listLeads","tags":["leads"],"summary":"List leads","description":"Returns leads the calling tenant can see, newest first. Cursor-paginated: pass the `next_cursor` from the previous response as `cursor`. Tenant-scoped through the authentication frame. Pass `unexported=true` to switch to the export queue (leads with `exported_to` still null) — this is what a puller like FounderReply's cron reads; add `warm=true` to further narrow that queue to leads with at least one reply on file.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Page of leads with `next_cursor`."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous response `next_cursor`.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Max items to return (default 50, max 200).","schema":{"type":"integer"}},{"name":"unexported","in":"query","required":false,"description":"When \"true\", list from the export queue (exported_to is null) instead of every lead.","schema":{"type":"boolean"}},{"name":"warm","in":"query","required":false,"description":"When \"true\" (only meaningful together with unexported=true), further narrow to leads with at least one sends row where replied_at is set.","schema":{"type":"boolean"}}]},"post":{"operationId":"createLead","tags":["leads"],"summary":"Create a lead","description":"Registers a lead. Runs through the intersect stage (suppression check + dedup) and returns the lead row. Returns 409 with a denial reason if intersect refuses the lead. Supply `campaign_id` (and a `context` note) to continue past the gate into personalize + dispatch — that generates the campaign's send rows, delivers step 0 inline, and queues later steps for the scheduler; the outcome is returned on a `pipeline` field. Set `enrich: true` to buy the \"why now\" fact from an enrichment vendor before personalizing: it runs after the gate (so a suppressed or duplicate lead never costs a lookup) and is billed per enriched lead, only on a citable result — a miss is reported on the `enrichment` field and costs nothing. An explicit `context` takes precedence over a vendor-supplied insight.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"201":{"description":"Lead created (with `pipeline` outcome when campaign_id was supplied)."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"campaign_id was supplied but no such campaign exists."},"409":{"description":"Lead suppressed, recently pitched, or otherwise denied by intersect; or `duplicate_lead` when a lead for the same company + product already exists."},"503":{"description":"`enrich: true` was requested but no enrichment provider is configured on this deployment. Returned before the gate runs and before any lead row is created — nothing is billed. Retry without `enrich`."}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional. Repeating a request with the same key replays the original response instead of acting twice.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"company_domain":{"type":"string","minLength":3,"maxLength":253},"company_name":{"type":"string","minLength":1,"maxLength":300},"target_product":{"type":"string","minLength":1,"maxLength":300},"best_contact_email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}]},"contact_role_class":{"anyOf":[{"type":"string","enum":["eng_lead","cto","founder","recruit","unknown"]},{"type":"null"}]},"jurisdiction":{"type":"string","minLength":2,"maxLength":2},"technical_hook_text":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}]},"technical_hook_verified":{"type":"boolean"},"fit_score":{"anyOf":[{"type":"integer","minimum":0,"maximum":100},{"type":"null"}]},"campaign_id":{"type":"string","minLength":1},"context":{"type":"string","maxLength":4000},"enrich":{"type":"boolean"}},"required":["company_domain","company_name","target_product","best_contact_email","contact_role_class","jurisdiction","technical_hook_text","technical_hook_verified","fit_score"],"additionalProperties":false}}}}}},"/api/v1/leads/{id}/ack-export":{"post":{"operationId":"ackLeadExport","tags":["leads"],"summary":"Acknowledge a lead export","description":"Called by the puller (e.g. FounderReply's cron) once it has durably ingested a lead pulled via GET /v1/leads?unexported=true. Sets exported_at/exported_to — exported_to is always the caller's own tenant id from its auth context, never client-supplied. Idempotent: acking an already-exported lead is a 200 no-op. A lead belonging to another tenant 404s (it is invisible under this tenant's RLS scope, same as any other /v1/leads lookup) rather than surfacing a cross-tenant conflict.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Export acknowledged (or already was)."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"No such lead visible to this tenant."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Lead id.","schema":{"type":"string"}}]}},"/api/v1/leads/{id}/thread":{"get":{"operationId":"getLeadThread","tags":["leads"],"summary":"What was sent to a lead","description":"Every step actually sent to the lead, oldest first: subject, body (markdown), sender address, recipient, Message-ID (bare, no angle brackets) and sent_at. With GET /v1/replies it gives a conversation tool (e.g. FounderReply) the whole thread, so it can answer in it. A lead belonging to another tenant 404s.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"`{ data: [{ send_id, step_index, message_id, from, to, subject, body_md, sent_at }] }`."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"No such lead visible to this tenant."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Lead id.","schema":{"type":"string"}}]}},"/api/v1/replies":{"get":{"operationId":"listReplies","tags":["leads"],"summary":"Replies not yet handed over","description":"With `unexported=true`: the text of replies to this workspace's sends that have not been acknowledged yet, oldest first (`limit`, default 100, max 200). Each carries lead_id, lead_email (the recipient of our send), send_id, the reply's own message_id, in_reply_to (our send's Message-ID), from_email, subject, text (quoted history cut) and intent (`reply`, or `opt_out` when it asked to stop — already suppressed here). A reply asking for erasure is never stored or listed. Text is held only until acknowledged (POST /v1/replies/{id}/ack) and at most 30 days. Without `unexported=true` the list is empty.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"`{ data: Reply[] }`."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"parameters":[{"name":"unexported","in":"query","required":false,"description":"`true` lists the replies not yet acknowledged.","schema":{"type":"boolean"}},{"name":"limit","in":"query","required":false,"description":"Page size (1-200, default 100).","schema":{"type":"integer"}}]}},"/api/v1/replies/{id}/ack":{"post":{"operationId":"ackReply","tags":["leads"],"summary":"Acknowledge a reply","description":"The puller has durably stored the reply: it is marked handed over and its text is dropped here. Idempotent: a repeated ack is a 200 no-op. 404 for an id that is not this workspace's.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"`{ ok: true }`."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"No such reply in this workspace."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Reply id (from GET /v1/replies).","schema":{"type":"string"}}]}},"/api/v1/sends":{"get":{"operationId":"listSends","tags":["sends"],"summary":"List sends for a lead","description":"Returns the dispatch history for a single lead, newest first. Requires the `lead_id` query parameter; cursor-paginated.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Page of send history with `next_cursor`.","content":{"application/json":{"schema":{"type":"object","properties":{"sends":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string"},"lead_id":{"type":"string"},"campaign_id":{"type":"string"},"step_index":{"type":"integer"},"to_email":{"type":"string"},"subject":{"type":"string"},"body_md":{"type":"string"},"message_id":{"type":"string"},"sender_domain":{"type":["string","null"],"description":"Sending domain this step went out from; null until sent."},"sender_address":{"type":["string","null"],"description":"Sender mailbox address this step went out from; null for relay sends, unsent steps, or older rows."},"scheduled_for":{"type":["string","null"]},"sent_at":{"type":["string","null"]},"resend_id":{"type":["string","null"],"description":"Transport message id (historical name)."},"delivered_at":{"type":["string","null"]},"bounced_at":{"type":["string","null"]},"bounce_reason":{"type":["string","null"]},"replied_at":{"type":["string","null"]},"unsubscribed_at":{"type":["string","null"]},"skipped_at":{"type":["string","null"]},"skipped_reason":{"anyOf":[{"type":"string","enum":["suppressed_between_steps","replied_already","quota_exceeded","lead_missing","recipient_missing","send_rejected","send_failed","prior_step_failed"]},{"type":"null"}],"description":"Set with skipped_at when the step was not sent (terminal)."},"deferred_reason":{"anyOf":[{"type":"string","enum":["daily_cap_deferred","mailbox_cap_deferred","mailbox_unavailable_deferred","domain_unavailable_deferred","send_retry_deferred","prior_step_pending"]},{"type":"null"}],"description":"Why a still-queued step was pushed back to scheduled_for (not terminal); null once sent or if never deferred."}},"required":["id","tenant_id","lead_id","campaign_id","step_index","to_email","subject","body_md","message_id","sender_domain","sender_address","scheduled_for","sent_at","resend_id","delivered_at","bounced_at","bounce_reason","replied_at","unsubscribed_at","skipped_at","skipped_reason","deferred_reason"],"additionalProperties":false}},"next_cursor":{"type":["string","null"]}},"required":["sends","next_cursor"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"parameters":[{"name":"lead_id","in":"query","required":true,"description":"Lead identifier.","schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous response `next_cursor`.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Max items to return (default 50, max 200).","schema":{"type":"integer"}}]}},"/api/v1/deliverability":{"get":{"operationId":"getDeliverability","tags":["sends"],"summary":"Sending health for your tenant","description":"Last 30 days: the sender domains that carried your mail (status, pause + reason, warm-up, daily cap), your sending reputation tier with the counts that decide it, bounce and complaint rates, and queued steps currently deferred by reason. Only your own data and public properties of domains that sent for you.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Deliverability snapshot.","content":{"application/json":{"schema":{"type":"object","properties":{"window_days":{"type":"integer","description":"Look-back window for every figure (days)."},"domains":{"type":"array","items":{"type":"object","properties":{"domain":{"type":"string"},"status":{"type":"string","description":"Sender-domain status: warming, active, paused, retired, … (unknown if no longer registered)."},"paused":{"type":"boolean"},"pause_reason":{"type":["string","null"],"description":"e.g. postmaster_breaker reasons, blocklist:dbl.spamhaus.org."},"paused_at":{"type":["string","null"]},"warmth_days":{"anyOf":[{"type":"integer"},{"type":"null"}]},"daily_cap":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Campaign sends per day this domain accepts across all its tenants."}},"required":["domain","status","paused","pause_reason","warmth_days","daily_cap"],"additionalProperties":false},"description":"Domains that carried your mail in the window, with their public health."},"egress_tier":{"type":"object","properties":{"pool":{"type":"string","enum":["probation","shared"],"description":"probation until you have enough clean volume; only applies to mail through our own MTA."},"window_days":{"type":"integer"},"sent":{"type":"integer"},"bounced":{"type":"integer"},"complaints":{"type":"integer"},"thresholds":{"type":"object","properties":{"min_sent":{"type":"number"},"max_bounce_rate":{"type":"number"},"max_complaints":{"type":"number"}},"required":["min_sent","max_bounce_rate","max_complaints"],"additionalProperties":false}},"required":["pool","window_days","sent","bounced","complaints","thresholds"],"additionalProperties":false,"description":"The sending reputation tier and the counts that decide it."},"rates":{"type":"object","properties":{"bounce_rate":{"type":["number","null"],"description":"bounced / sent in the window; null with no sends."},"complaint_rate":{"type":["number","null"]}},"required":["bounce_rate","complaint_rate"],"additionalProperties":false},"deferred":{"type":"object","additionalProperties":{"type":"integer"},"description":"Queued steps currently pushed back, by deferred_reason."}},"required":["window_days","domains","egress_tier","rates","deferred"],"additionalProperties":false}}}},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/domains":{"get":{"operationId":"listDomainOrders","tags":["domains"],"summary":"List domain orders","description":"Your bought and connected sending domains, newest first. Each order carries its state, a step timeline, and `next_action` (e.g. the nameservers to set for a bring-your-own domain). Optional `limit` (max 200).\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"`{ orders: DomainOrder[] }`."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"parameters":[{"name":"limit","in":"query","required":false,"description":"Max orders (default 100, max 200).","schema":{"type":"integer"}}]},"post":{"operationId":"buyDomain","tags":["domains"],"summary":"Buy a sending domain","description":"Registers the domain (Porkbun), hosts its DNS, checks its reputation, publishes SPF/DKIM/DMARC/MX, creates `mailbox_count` mailboxes and starts warming them — asynchronously; poll `GET /api/v1/domains/{id}`. Quote first (`POST /api/v1/domains/quote`): a purchase inside your plan's included domains costs nothing extra; beyond it, the customer's prepaid credit pays registrar cost + markup, and you MUST echo the quote's `charge_micro_usd` as `confirm_charge_micro_usd` — otherwise nothing is bought (409). Same `idempotency_key` (or Idempotency-Key header) ⇒ same order. Returns 503 `domain_purchases_disabled` until buying is enabled for your account. Admin minimum.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Idempotent replay; `{ replayed: true, order }`."},"201":{"description":"Order created; `{ replayed: false, order }`."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"402":{"description":"`plan_domain_quota_exceeded`, `payment_customer_required` or `insufficient_credit`."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"`customer_not_found`."},"409":{"description":"`charge_confirmation_required` / `charge_mismatch` (body has charge_micro_usd), `domain_not_available`, `premium_domain`, `already_ordered` (body has order_id), `registrar_rejected`."},"422":{"description":"`idempotency_key_reused` for a different domain."},"429":{"description":"Too many purchases for this key (20 / hour)."},"503":{"description":"`domain_purchases_disabled` or `registrar_unavailable`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string","minLength":4,"maxLength":253},"mailbox_count":{"type":"integer","minimum":1,"maximum":25,"description":"Mailboxes to create on the domain (default 3)."},"customer_id":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"description":"End-customer whose prepaid credit pays when the purchase is beyond the plan's included domains."},"confirm_charge_micro_usd":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"description":"Echo of the quote's charge_micro_usd. Required when the purchase is not covered by the plan — nothing is bought unseen."},"idempotency_key":{"type":"string","minLength":8,"maxLength":200,"description":"Retrying with the same key returns the same order (the Idempotency-Key header works too)."}},"required":["domain"],"additionalProperties":false}}}}}},"/api/v1/domains/search":{"get":{"operationId":"searchDomains","tags":["domains"],"summary":"Search available domains","description":"Availability and our price (registrar cost + markup, micro-USD and USD) for a word (`q=acme` → acme.com, acme.io, … and outreach-style variants) or explicit `names`. Premium names are never purchasable. Buys nothing. Member minimum; 20 searches / minute.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"`{ results: [{ domain, available, premium, purchasable, price_micro_usd, price_usd, … }], purchases_enabled, markup_pct }`."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"503":{"description":"`registrar_unavailable`."}},"parameters":[{"name":"q","in":"query","required":false,"description":"A word or a full domain name.","schema":{"type":"string"}},{"name":"names","in":"query","required":false,"description":"Comma-separated full domain names to check (max 8).","schema":{"type":"string"}},{"name":"tlds","in":"query","required":false,"description":"Comma-separated TLDs to try for `q` (default: the allowed catalog).","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Max names to check (default 5, max 8).","schema":{"type":"integer"}}]}},"/api/v1/domains/quote":{"post":{"operationId":"quoteDomain","tags":["domains"],"summary":"Quote a domain purchase","description":"What buying the domain costs you right now: `covered_by_plan` (one of your plan's included domains) or `charge_micro_usd` from `customer_id`'s credit, with the balance, `can_purchase` and `reasons`. Buys nothing. Member minimum.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"`{ quote }`."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"`customer_not_found`."},"503":{"description":"`registrar_unavailable`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string","minLength":4,"maxLength":253},"customer_id":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]}},"required":["domain"],"additionalProperties":false}}}}}},"/api/v1/domains/connect":{"post":{"operationId":"connectDomain","tags":["domains"],"summary":"Connect your own domain","description":"Bring a domain you already own. We create its DNS zone and return the two nameservers to set at your registrar (`instructions.nameservers`, 'pending' for the first minute — poll the order). Once they point to us we check the domain's reputation, publish SPF/DKIM/DMARC/MX, create the mailboxes and warm them. Uses one of your plan's included domains (402 when none are left). Use a domain you don't use for everyday email. Admin minimum.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Idempotent replay."},"201":{"description":"`{ replayed: false, order, instructions }`."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"402":{"description":"`plan_domain_quota_exceeded`."},"403":{"description":"Authenticated but role does not permit the action."},"409":{"description":"`already_ordered` (body has order_id)."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string","minLength":4,"maxLength":253},"mailbox_count":{"type":"integer","minimum":1,"maximum":25,"description":"Mailboxes to create on the domain (default 3)."},"customer_id":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"idempotency_key":{"type":"string","minLength":8,"maxLength":200,"description":"Retrying with the same key returns the same order (the Idempotency-Key header works too)."}},"required":["domain"],"additionalProperties":false}}}}}},"/api/v1/domains/assess":{"get":{"operationId":"assessDomain","tags":["domains"],"summary":"Check a domain’s reputation","description":"Read-only reputation check: Spamhaus DBL, SURBL and URIBL listings, domain age (RDAP), SPF/DKIM/DMARC/MX, parked or live website, and signs the domain is already used for other mail. Returns a 0–100 score, a verdict (good | caution | reject — any blocklist listing rejects) and every signal. Member minimum; 20 checks / minute.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"`{ assessment: { domain, score, verdict, reasons, signals, checked_at } }`."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"parameters":[{"name":"domain","in":"query","required":true,"description":"Domain to check.","schema":{"type":"string"}},{"name":"source","in":"query","required":false,"description":"`byo` (default) or `purchase` — age and prior use only count for byo.","schema":{"type":"string"}},{"name":"stage","in":"query","required":false,"description":"`pre-dns` (default) or `post-dns` — SPF/DKIM/DMARC/MX only count post-dns.","schema":{"type":"string"}}]}},"/api/v1/domains/{id}":{"get":{"operationId":"getDomainOrder","tags":["domains"],"summary":"Get a domain order","description":"One order by id (or by domain name): state, `steps` timeline (done / current / pending / failed), `next_action` for you, reputation summary, warming numbers and mailbox addresses (credentials are never returned).\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"`{ order }`."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"No such order."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Order id or domain name.","schema":{"type":"string"}}]},"delete":{"operationId":"cancelDomainOrder","tags":["domains"],"summary":"Cancel a domain order","description":"Cancels an order that has not bought anything: a bring-your-own order before warming, or a purchase before it is registered. Any credit charged is refunded. Admin minimum.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"`{ order }` (state cancelled)."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"No such order."},"409":{"description":"`not_cancellable` (already registered, warming, or finished)."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Order id.","schema":{"type":"string"}}]}},"/api/v1/privacy/erasure":{"post":{"operationId":"eraseDataSubject","tags":["suppressions"],"summary":"Erase a person's data (GDPR Art. 17 / CCPA delete)","description":"Removes the address from your leads (and their enrichment), cancels queued steps to it, redacts sent emails (recipient, subject, body), deletes verification and bounce rows, and adds a suppression so the person is never contacted again. Logged (hash only). Also triggered automatically when a recipient asks for it in a reply or with the \"Delete my data\" button on the unsubscribe page — subscribe to the `privacy.erasure` webhook to mirror it in your own systems.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"`{ request_id, erased: { leads, sends_redacted, sends_cancelled, verifications, soft_bounces, llm_previews }, suppressed: true }`"},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"}},"required":["email"],"additionalProperties":false}}}}}},"/api/v1/privacy/export":{"post":{"operationId":"exportDataSubject","tags":["suppressions"],"summary":"Everything held about a person (GDPR Art. 15 access)","description":"Returns every lead, sent email (with content), suppression and verification this workspace holds for the address — the answer to a subject access request. POST so the address never appears in URLs or logs.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"`{ request_id, email, generated_at, leads, sends, suppressions, verifications }`"},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"}},"required":["email"],"additionalProperties":false}}}}}},"/api/v1/account/export":{"get":{"operationId":"exportMyPersonalData","tags":["tenants"],"summary":"Download your own personal data (GDPR Art. 15 / 20)","description":"The signed-in person’s own data as a versioned JSON attachment: profile, sign-in methods, MFA factors (metadata), workspace memberships, pending invitations, legal acceptances, support requests, API keys they created (metadata) and the actions they performed, with sources and retention. Never includes secrets or other people’s data. Session only (an API key gets 403), MFA must be satisfied, 5 per hour.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"JSON attachment `cognilead-personal-data-<date>.json` (`private, no-store`)."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Called with an API key (`session_required`) or MFA pending (`mfa_required`)."},"429":{"description":"More than 5 exports in an hour (`rate_limited`)."},"503":{"description":"Memberships could not be read (`export_unavailable`) — refused rather than incomplete."}}}},"/api/v1/suppressions":{"get":{"operationId":"listSuppressions","tags":["suppressions"],"summary":"List suppressions","description":"Returns suppression entries (email- or domain-keyed) for the calling tenant, newest first. Cursor-paginated.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Page of suppressions with `next_cursor`."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous response `next_cursor`.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Max items to return (default 50, max 200).","schema":{"type":"integer"}}]},"post":{"operationId":"createSuppression","tags":["suppressions"],"summary":"Add a suppression","description":"Adds a suppression row that blocks future outreach to the email or domain. At least one of email or domain must be provided. Idempotent on the value: suppressing an address or domain that is already on the list returns 200 `{ email, domain, already_suppressed: true }` instead of an error.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Already suppressed — `{ email, domain, already_suppressed: true }`; nothing changed."},"201":{"description":"Suppression created."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional. Repeating a request with the same key replays the original response instead of acting twice.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"domain":{"type":"string","maxLength":253},"reason":{"type":"string","enum":["unsubscribe","bounce_hard","bounce_soft_repeat","reply_negative","gdpr_request","manual"]}},"required":["reason"],"additionalProperties":false}}}}}},"/api/v1/resend/step":{"post":{"operationId":"dispatchStep","tags":["dispatch"],"summary":"Dispatch a sequence step","description":"Drives one step of an outreach sequence. The in-process scheduler calls dispatchStep() directly; this HTTP entry point exists for operators driving cadence from a Resend Automation or external job runner.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Dispatch outcome: `sent`, `skipped` or `error`, with a `reason`. `daily_cap_deferred` / `mailbox_cap_deferred` mean the step was pushed to the next UTC day (still queued), not dropped.","content":{"application/json":{"schema":{"type":"object","properties":{"outcome":{"type":"string","enum":["sent","skipped","error"]},"reason":{"type":"string","enum":["suppressed_between_steps","replied_already","quota_exceeded","lead_missing","recipient_missing","send_rejected","send_failed","prior_step_failed","daily_cap_deferred","mailbox_cap_deferred","mailbox_unavailable_deferred","domain_unavailable_deferred","send_retry_deferred","prior_step_pending","already_sent","previously_skipped","send_not_found"],"description":"skipped + a skip reason: terminal. skipped + a *_deferred reason: NOT terminal — the step stays queued (caps: next UTC day; unusable domain/mailbox: an hour later) and carries deferred_reason. sent + already_sent / skipped + previously_skipped: idempotent repeat. error + send_not_found: unknown id."},"resend_id":{"type":"string","description":"Transport message id; set when outcome is sent."},"send_id":{"type":"string"}},"required":["outcome","send_id"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"Send not found in the calling tenant."},"500":{"description":"Unexpected server error."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"send_id":{"type":"string","minLength":1}},"required":["send_id"],"additionalProperties":false}}}}}},"/api/v1/images/generate":{"post":{"operationId":"generateImage","tags":["media"],"summary":"Generate an image","description":"Generates an image through the PHI Gateway (Gemini Imagen in production). Counts against the same global LLM budget cap as text generation.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Generated images and cost in micro-USD."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"No image surface registered with the active gateway."},"500":{"description":"Unexpected server error."},"503":{"description":"LLM budget cap exceeded."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":4000},"aspect_ratio":{"type":"string","enum":["1:1","16:9","9:16","4:3","3:4"]},"n":{"type":"integer","minimum":1,"maximum":4}},"required":["prompt"],"additionalProperties":false}}}}}},"/api/v1/campaigns":{"get":{"operationId":"listCampaigns","tags":["campaigns"],"summary":"List campaigns","description":"Returns every campaign for the calling tenant (any status), cursor-paginated.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"A page of campaigns and a `next_cursor`."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"500":{"description":"Unexpected server error."}},"parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous response `next_cursor`.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Max items to return (default 50, max 200).","schema":{"type":"integer"}}]},"post":{"operationId":"createCampaign","tags":["campaigns"],"summary":"Create a campaign","description":"Creates a campaign (id generated server-side). `steps` defaults to a single immediate step and `status` to `draft` when omitted. `sender_domain` is reserved and ignored — sending domains and mailboxes are assigned by CogniLead's sender pool at send time; it is accepted for backward compatibility. Send an optional `idempotency-key` header to make retries safe.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"201":{"description":"The created campaign."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"500":{"description":"Unexpected server error."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"target_product":{"type":"string","minLength":1},"prompt_template_id":{"type":"string","minLength":1},"sender_domain":{"type":"string","minLength":1,"description":"Reserved and currently ignored: sending domains and mailboxes are assigned by CogniLead's sender pool at send time. Accepted for backward compatibility; omit it."},"steps":{"type":"array","items":{"type":"object","properties":{"delay_days":{"type":"integer","minimum":0},"subject":{"type":"string"},"body_md":{"type":"string"}},"required":["delay_days"],"additionalProperties":false},"minItems":1},"status":{"type":"string","enum":["draft","active","paused","archived"]}},"required":["target_product","prompt_template_id"],"additionalProperties":false}}}}}},"/api/v1/campaigns/{id}":{"patch":{"operationId":"updateCampaign","tags":["campaigns"],"summary":"Update a campaign status","description":"Mutates a campaign lifecycle status (draft ↔ active ↔ paused ↔ archived). The product / prompt / sender binding is immutable once created. Backs the dashboard Pause/Resume action.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"The updated campaign."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"Campaign not found in the calling tenant."},"500":{"description":"Unexpected server error."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Campaign id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["draft","active","paused","archived"]}},"required":["status"],"additionalProperties":false}}}}}},"/api/v1/campaigns/{id}/run":{"post":{"operationId":"runCampaign","tags":["campaigns"],"summary":"Run a campaign","description":"Dispatches any currently-due steps for the campaign through the send pipeline — suppression, reply, and quota gates re-fire. Optionally restrict to `lead_ids`. Returns a dispatch summary; `202` when the campaign was only enqueued (nothing currently due).\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Dispatch outcome (`processed`, `sent`, `skipped`, `errors`, `reasons`, `send_ids`)."},"202":{"description":"Campaign enqueued; nothing currently due to dispatch."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"Campaign not found in the calling tenant."},"500":{"description":"Unexpected server error."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Campaign id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"lead_ids":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}]}},"additionalProperties":false}}}}}},"/api/v1/live":{"get":{"operationId":"liveChannel","tags":["live"],"summary":"Live channel snapshot","description":"Compact, tenant-scoped snapshot for a live-updating dashboard surface (`pool` | `sends` | `usage`) — counts and latest timestamps only. Returns a strong `ETag`; send `If-None-Match` to get `304 Not Modified` when unchanged. Always `Cache-Control: private, no-store`. Powers the dashboard partial-poll live layer.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"The channel snapshot (with ETag)."},"304":{"description":"Not modified — ETag matched If-None-Match."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"500":{"description":"Unexpected server error."}},"parameters":[{"name":"channel","in":"query","required":true,"description":"Which snapshot to return: pool | sends | usage.","schema":{"type":"string"}}]}},"/api/v1/tenants":{"get":{"operationId":"listTenants","tags":["tenants"],"summary":"List my tenants","description":"Returns every tenant the signed-in user is a member of, paired with their role.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"List of tenants with role."},"401":{"description":"Authentication required or invalid."}}},"post":{"operationId":"createTenant","tags":["tenants"],"summary":"Create a tenant","description":"Creates a new tenant and adds the signed-in user as `owner`. Slug must be unique. Session-only — a freshly signed-up user must be able to bootstrap their first tenant before any API key exists.","security":[{"SessionCookie":[]}],"responses":{"201":{"description":"Tenant created."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"409":{"description":"Slug already in use."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":120},"slug":{"type":"string","minLength":2,"maxLength":60,"pattern":"^[a-z0-9-]+$"},"primary_jurisdiction":{"type":"string","minLength":2,"maxLength":2},"accept_aup":{"type":["boolean","string"],"enum":[true,"true","on"]}},"required":["name","slug","primary_jurisdiction"],"additionalProperties":false}}}}}},"/api/v1/tenants/{id}/members":{"get":{"operationId":"listMembers","tags":["tenants"],"summary":"List tenant members","description":"Returns every member of the tenant. Caller must be a member of the tenant.\n\n**Minimum role:** `viewer`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"List of members with role."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Tenant identifier.","schema":{"type":"string"}}]},"post":{"operationId":"addMember","tags":["tenants"],"summary":"Add a member","description":"Adds a member to the tenant with the given role. Admin minimum.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"201":{"description":"Member added."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Tenant identifier.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","maxLength":320},"user_id":{"type":"string","minLength":1,"maxLength":320},"role":{"type":"string","enum":["owner","admin","member","viewer"]}},"required":["role"],"additionalProperties":false}}}}},"patch":{"operationId":"updateMember","tags":["tenants"],"summary":"Change a member role","description":"Changes a member role. Owner-only. Enforces the last-owner invariant — the final owner cannot be demoted.\n\n**Minimum role:** `owner`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Member updated."},"400":{"description":"Invalid body or would remove the last owner."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Tenant identifier.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"string","minLength":1},"role":{"type":"string","enum":["owner","admin","member","viewer"]}},"required":["user_id","role"],"additionalProperties":false}}}}},"delete":{"operationId":"removeMember","tags":["tenants"],"summary":"Remove a member","description":"Removes a member from the tenant. Admin minimum. Enforces the last-owner invariant.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Member removed."},"400":{"description":"Would remove the last owner."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Tenant identifier.","schema":{"type":"string"}},{"name":"user_id","in":"query","required":true,"description":"User to remove.","schema":{"type":"string"}}]}},"/api/v1/billing/checkout":{"post":{"operationId":"createCheckout","tags":["billing"],"summary":"Create Stripe Checkout session","description":"Creates a Stripe Checkout session for a tier/sku in the plan catalog. Returns the hosted Checkout URL. Free tier and enterprise are rejected.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Checkout session URL."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"500":{"description":"Unexpected server error."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tier":{"type":"string"},"sku":{"type":"string"},"billing_period":{"type":"string","enum":["monthly","annual"]},"success_url":{"type":"string","format":"uri"},"cancel_url":{"type":"string","format":"uri"}},"required":["tier","sku"],"additionalProperties":false}}}}}},"/api/v1/billing/portal":{"post":{"operationId":"createPortal","tags":["billing"],"summary":"Create Stripe Customer Portal session","description":"Creates a Stripe Customer Portal session for the calling tenant. 404 if the tenant has no billing customer yet (no completed checkout).\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Portal URL."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"No Stripe customer for the tenant."},"500":{"description":"Unexpected server error."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"return_url":{"type":"string","format":"uri"}},"additionalProperties":false}}}}}},"/api/v1/billing/subscription":{"get":{"operationId":"getSubscription","tags":["billing"],"summary":"Get current subscription + plan","description":"Returns the active subscription and the resolved PlanRow. A null subscription with `defaultedToFree: true` signals the free-tier empty state.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Subscription and plan."},"401":{"description":"Authentication required or invalid."}}}},"/api/v1/billing/usage":{"get":{"operationId":"getUsage","tags":["billing"],"summary":"Get current-period usage","description":"Returns current-period send count, cap, and projected overage cost in micro-USD.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Usage snapshot."},"401":{"description":"Authentication required or invalid."}}}},"/api/v1/internal/status":{"get":{"operationId":"getPlatformStatus","tags":["status"],"summary":"Platform health verdict","description":"Operator only. ok | degraded | down with reasons: database reachability, scheduler heartbeat, job queue backlog, API 5xx rate over the last 10 minutes, subsystem failures/overdue runs and open incidents. Returns 503 when down so a plain HTTP monitor can alert on status code.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"ok or degraded."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"503":{"description":"down."}}}},"/api/v1/audit-events":{"get":{"operationId":"listAuditEvents","tags":["audit"],"summary":"List audit events","description":"The workspace append-only audit trail: API key, member, webhook, calendar, erasure and operator safety changes, newest first. Snapshots are redacted (no secrets). Admin minimum.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Audit events."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"parameters":[{"name":"limit","in":"query","required":false,"description":"Max events (default 100, max 500).","schema":{"type":"integer"}}]}},"/api/v1/keys":{"get":{"operationId":"listKeys","tags":["api-keys"],"summary":"List API keys","description":"Returns the calling tenant API keys with prefix and last4 only. The full secret and the hash never leave the server. Admin minimum.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"API key list."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}},"post":{"operationId":"createKey","tags":["api-keys"],"summary":"Mint an API key","description":"Mints a fresh API key. The cleartext secret is returned ONCE in the response body — copy it immediately. Admin minimum.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"201":{"description":"Key created, secret returned ONCE."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"role":{"type":"string","enum":["admin","member","viewer"]}},"required":["name"],"additionalProperties":false}}}}}},"/api/v1/keys/{id}":{"delete":{"operationId":"revokeKey","tags":["api-keys"],"summary":"Revoke an API key","description":"Soft-deletes an API key (stamps revoked_at). Past api_calls referencing it stay intact. Admin minimum.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Key revoked."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"Key not found."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Key identifier.","schema":{"type":"string"}}]}},"/api/v1/internal/pool/stats":{"get":{"operationId":"getInternalPoolStats","tags":["pool"],"summary":"Pool stats (operator)","description":"Returns counts per mailbox state (ready/warming/assigned/cooling_off/retired), the domain-pool breakdown and the last replenish run for the REAL warming pool (rows in the pool tenant). Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Pool stats snapshot."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/pool/reserve":{"post":{"operationId":"reserveInternalPoolMailboxes","tags":["pool"],"summary":"Reserve mailboxes (operator)","description":"Atomically transitions N pool mailboxes from ready|warming → assigned and stamps assigned_to_tenant_id. The reserve runs SELECT … FOR UPDATE SKIP LOCKED in postgres so concurrent calls never double-assign. Reserves from the real pool (ready mailboxes). Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Assigned mailbox rows."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer","minimum":1,"maximum":100},"tenant_id":{"type":"string","minLength":1}},"required":["count"],"additionalProperties":false}}}}}},"/api/v1/internal/pool/cool-off":{"post":{"operationId":"coolOffInternalPoolMailboxes","tags":["pool"],"summary":"Cool-off mailboxes (operator)","description":"Transitions a set of assigned mailboxes to cooling_off, stamping cooling_off_until = now + hours (default 168 = 7 days). The mailboxes return to ready automatically when the timer elapses. Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Mailboxes transitioned to cooling_off."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mailbox_ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1},"cool_off_hours":{"type":"integer","minimum":1,"maximum":720}},"required":["mailbox_ids"],"additionalProperties":false}}}}}},"/api/v1/internal/pool/replenish":{"post":{"operationId":"replenishInternalPool","tags":["pool"],"summary":"Trigger pool replenish","description":"Calls the warming engine ensurePoolFloor() routine to top up the pool to WARMING_POOL_MIN_READY. Returns the number of mailboxes provisioned. Safe to call repeatedly — no-op when the pool is already at the floor. Runs in the pool tenant; the run is recorded (subsystem_runs, name pool-replenish). Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Replenish run result."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"503":{"description":"Warming engine module not registered."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"batch":{"type":"integer","minimum":1,"maximum":50}},"additionalProperties":false}}}}}},"/api/v1/internal/pool/warming-pass-all":{"post":{"operationId":"runInternalPoolWarmingPassAll","tags":["pool"],"summary":"Run today's warming pass now","description":"Runs the scheduler's daily warming routine immediately (domain ramp, peer-to-peer exchange, pool floor). Idempotent per UTC day: once today's budget is sent, it is a no-op, so it can never exceed a domain's ramp. `touched` = warming messages sent by this call. Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Warming pass batch result."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"503":{"description":"Warming engine module not registered."}}}},"/api/v1/internal/pool/health":{"get":{"operationId":"getInternalPoolHealth","tags":["pool"],"summary":"Background-job health","description":"Per scheduler subsystem (imap, postmaster, warming, blocklist, pool-acquire): enabled, configured (inputs present — values are never returned), cadence, and the last recorded run (started/finished, ok, summary, error, run count). Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ subsystems: [...] }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/pool/transports":{"get":{"operationId":"listInternalPoolTransports","tags":["pool"],"summary":"Pool mailbox transport inventory","description":"Per pool mailbox: address, domain, state, sending backend (relay | provider | own_mta) and vendor, whether SMTP and IMAP are configured, and today's sends against the per-inbox daily cap. Credentials (usernames, sealed passwords) are never returned. Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ mailboxes: [...] }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/pool/capacity":{"get":{"operationId":"getInternalPoolCapacity","tags":["pool"],"summary":"Own-MTA IP capacity forecast","description":"Per egress pool (warming, shared, probation): warm capacity vs required volume (demand × margin + one warm reserve IP) now, at T+21 and at the warm-up horizon, status (ok | watch | order_now), the mailboxes/domains needed to emit that volume, and dated actions (start warming, schedule cohorts, promote, order IPs, add mailboxes). Also `ip_onboarding`: the hostname of each MTA IP and onboarding step (A record → forward DNS → PTR → forward-confirmed reverse DNS → blocklists → pool), status done | wait | blocked and what it waits on. IP names/hostnames only — never addresses. Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ plan: {...}, ip_onboarding: { pass, ips: [{ hostname, state, status, reason, checked_at }] } | null }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/pool/autobuy":{"get":{"operationId":"getInternalPoolAutobuy","tags":["pool"],"summary":"Automatic sending-pool domains","description":"Our own sending pool buys its domains through the product buy flow on a schedule (capacity planner need − owned − in flight). Shows whether it is on/paused, domains needed vs owned vs in flight, spend this month vs cap, per-domain price cap, buys today vs the daily limit, the last run and recent pool orders. Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ autobuy: { enabled, paused, domains_needed, domains_have, in_flight, need, spent_month_usd, cap_month_usd, bought_today, max_per_day, last_run, recent: [...] } }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}},"post":{"operationId":"setInternalPoolAutobuyPaused","tags":["pool"],"summary":"Pause or resume automatic pool-domain buying","description":"Body `{ \"paused\": true | false, \"reason\"?: string }`. Takes effect on the next scheduler run; no env change or redeploy. Automatic buying also pauses itself after a registrar refusal. Operator only.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ autobuy: {...} }` — the status after the change"},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/abuse":{"get":{"operationId":"getInternalAbuse","tags":["pool"],"summary":"Abuse + feedback-loop handling","description":"The app reads every MTA server's ops mailbox (abuse@/postmaster@ of all pool domains and of the ops domain), correlates each report to our sends, suppresses the recipient platform-wide, pauses tenants / sender domains past complaint thresholds and acknowledges human abuse desks. Shows reports received (24h / 7d by kind), complaints and IP reports over 7 days, which ops mailboxes the app reads (never credentials), the thresholds in force and the last 20 reports with the actions taken. Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ abuse: { ops_mailbox, inboxes: [...], reports_24h, reports_7d, complaints_7d, ip_reports_7d, thresholds, recent: [...] } }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/postmaster/oauth":{"get":{"operationId":"getInternalPostmaster","tags":["pool"],"summary":"Google Postmaster connection and per-domain health","description":"Whether a Google account is connected (account, granted scopes, broken state — never the token), the OAuth client in use (id only) and the exact redirect URI to register, plus every sending domain's Postmaster registration state (verified / site_verified / awaiting_dns / not ours) and latest daily health (reputation rejects, spam rate, auth pass rates, compliance). Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ postmaster: { connection: {...}, domains: [{ domain, registration, health }] } }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}},"post":{"operationId":"setInternalPostmasterClient","tags":["pool"],"summary":"Save the Google OAuth client","description":"Body `{ \"client_id\": \"…apps.googleusercontent.com\", \"client_secret\": \"…\" }` — the Web-application OAuth client the one-time Connect Google uses. The secret is sealed and never returned. Env POSTMASTER_OAUTH_CLIENT_ID/_SECRET take precedence. Operator only.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ postmaster: {...} }` — status after saving"},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}},"delete":{"operationId":"disconnectInternalPostmaster","tags":["pool"],"summary":"Disconnect Google Postmaster","description":"Revokes the stored refresh token at Google (best effort) and forgets it. Domain registration and daily stats stop until Google is connected again. Operator only.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ postmaster: {...} }` — status after disconnecting"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/postmaster/oauth/start":{"get":{"operationId":"startInternalPostmasterOAuth","tags":["pool"],"summary":"Start the one-time Google consent","description":"Redirects (303) to Google's consent screen with offline access, PKCE (S256) and a state bound to a sealed, HttpOnly, 10-minute cookie scoped to the callback path. Scopes: openid, email, postmaster.domain, postmaster.traffic.readonly, siteverification.verify_only. Optional `?login_hint=` email. Operator only.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"303":{"description":"Redirect to accounts.google.com (or back to the Pool page when no OAuth client is configured)"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/postmaster/oauth/callback":{"get":{"operationId":"finishInternalPostmasterOAuth","tags":["pool"],"summary":"Google consent callback","description":"Google redirects here with `code` + `state`. The state must match the cookie set by /start (constant time, under 10 minutes); the code is exchanged with the PKCE verifier and the refresh token is stored sealed. Always redirects back to /dashboard/pool?tab=setup&postmaster=<outcome>. Operator only.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"303":{"description":"Back to the Pool page; `postmaster=connected | denied | bad_state | no_client | no_refresh_token | …`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/calendar/oauth/{provider}/start":{"get":{"operationId":"startCalendarOAuth","tags":["tenants"],"summary":"Start connecting a calendar","description":"Dashboard flow: redirects (303) to the provider's consent screen — `provider` is `google` (Google Calendar) or `microsoft` (Microsoft 365, any Microsoft account) — with PKCE (S256) and a state bound to this workspace and user in a sealed, HttpOnly, 10-minute cookie scoped to the callback path. Read-only scopes (Google calendar.events.readonly; Microsoft Calendars.Read, User.Read, offline_access). Optional `?login_hint=` email.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"303":{"description":"Redirect to the provider (or back to /dashboard/calendar when the provider is not configured)"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"Unknown provider (only google and microsoft)."}},"parameters":[{"name":"provider","in":"path","required":true,"description":"`google` or `microsoft`.","schema":{"type":"string","enum":["google","microsoft"]}}]}},"/api/v1/calendar/oauth/{provider}/callback":{"get":{"operationId":"finishCalendarOAuth","tags":["tenants"],"summary":"Calendar consent callback","description":"The provider redirects here with `code` + `state`. The cookie from /start must match the state, provider, workspace and user (under 10 minutes); the code is exchanged with the PKCE verifier and the tokens are stored sealed for this workspace. Always redirects to /dashboard/calendar?calendar=<outcome>&provider=<provider>.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"303":{"description":"Back to the Calendar page; `calendar=connected | denied | scopes_missing | bad_state | no_client | …`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"Unknown provider (only google and microsoft)."}},"parameters":[{"name":"provider","in":"path","required":true,"description":"`google` or `microsoft`.","schema":{"type":"string","enum":["google","microsoft"]}}]}},"/api/v1/internal/postmaster/snds":{"get":{"operationId":"getInternalSnds","tags":["pool"],"summary":"Microsoft SNDS feeds and IP standing","description":"Whether the SNDS Automated Data Access feeds are set (host, set/expiry dates, last fetch — never the key), each of our IPs' Outlook.com standing (filter result GREEN/YELLOW/RED, complaint rate, trap hits, blocked), the IP ranges still to request on SNDS \"Request Access\" with the addresses SNDS will mail (postmaster@/abuse@ of the PTR domain, read by the app), and the authorization mails the app followed. Operator only.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ snds: { feed: {...}, ips: [...], access: [...], authorizations: [...] } }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}},"post":{"operationId":"setInternalSndsFeed","tags":["pool"],"summary":"Save the SNDS automated-access URLs","description":"Body `{ \"data_url\"?: string, \"ip_status_url\"?: string }` — https URLs on substrate.office.com (or the legacy SNDS host) only. Stored sealed. SNDS automated links expire after 30 days; the platform warns a week ahead. Operator only.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ snds: {...} }` — status after saving"},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/placement":{"get":{"operationId":"getInternalPlacement","tags":["pool"],"summary":"Seed network + inbox placement","description":"Seed mailboxes we own at Google Workspace, Microsoft 365 and IMAP providers warm with the pool (they open, rescue from spam, star and reply through the provider API) and receive daily placement probes. Shows the seeds per provider and status (never credentials), monthly seat cost, the warming exchange with seeds over 7 days, and — per sending IP and per sending domain, per provider family — inbox / promotions / spam / missing counts and the inbox rate over 7 days, flagged degraded under the alert threshold. Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ placement: { providers, autoprovision, seeds: [...], seeds_by_provider, monthly_seat_usd, window_days, threshold, min_probes, probes_7d, pending, rows: [{ kind, target, families, worst_rate, degraded }], warming_7d } }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/placement/seeds":{"post":{"operationId":"manageInternalPlacementSeeds","tags":["pool"],"summary":"Register / disable / enable a seed mailbox","description":"Body `{ \"action\": \"register\", \"address\", \"family\": \"gmail\"|\"microsoft\"|\"yahoo\"|\"other\", \"role\": \"warming\"|\"probe\"|\"both\", \"imap\": { \"host\", \"port\", \"secure\", \"username\"?, \"password\", \"junk_folder\"? } }` registers an IMAP-reachable seed at a provider that cannot be created by API (the password is sealed at rest and never returned); `{ \"action\": \"disable\"|\"enable\", \"id\" }` takes a seed out of rotation or puts it back (health-checked first). Google Workspace / Microsoft 365 seeds are created by the seed-network subsystem itself. Operator only.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ ok: true }` (disable / enable)"},"201":{"description":"`{ seed: { id, address, status } }` (register)"},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"`not_found` — no seed with that id (disable / enable)."}}}},"/api/v1/internal/privacy":{"get":{"operationId":"getInternalPrivacy","tags":["pool"],"summary":"Privacy request log and platform-wide suppressions","description":"The data-subject request log (erasures, exports, objections — email hash only, scope, who asked, what was done) and the platform-wide suppression list (complaints, non-existent mailboxes, platform erasures; hashes). Operator only.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ requests: [...], global_suppressions: [...] }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}},"post":{"operationId":"actInternalPrivacy","tags":["pool"],"summary":"Erase an address platform-wide, or take it off the platform list","description":"Body `{ \"action\": \"erase\" | \"unsuppress\", \"email\" }`. Erase runs the erasure in every workspace (workspaces that held data get a `privacy.erasure` webhook) and puts the address on the platform-wide list as a hash, so future workspaces cannot contact it either. Unsuppress removes a platform-wide entry. Operator only.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ request_id, tenants_with_data, erased }` or `{ removed }`"},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/tenant-safety":{"get":{"operationId":"getInternalTenantSafety","tags":["pool"],"summary":"Customer workspaces and their sending status","description":"Every customer workspace with its sending status (active / limited / suspended) and why, its signup risk score, 7-day sent and bounce counts and today's ramp ceiling (lib/pipeline/tenant-safety). Operator only: session auth, admin role, and the operator tenant.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ customers: [{ tenant_id, name, plan, created_at, safety, sent_7d, bounced_7d, ceiling_today }] }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}},"post":{"operationId":"setInternalTenantSafety","tags":["pool"],"summary":"Suspend, limit or reinstate a customer workspace","description":"Body `{ \"tenant_id\", \"action\": \"suspend\" | \"limit\" | \"reinstate\", \"reason\"? }`. Suspend also pauses the workspace's active campaigns (queued steps wait); an operator suspension is never lifted automatically. Limit and reinstate restart the sending ramp. Operator only.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ customers: [...] }` — the view after the change"},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"`not_found` — no such workspace"}}}},"/api/v1/internal/mta/servers/enroll":{"post":{"operationId":"enrollMtaServer","tags":["mta"],"summary":"MTA server self-enrollment","description":"Called by infra/mta/bootstrap/bootstrap.sh at the end of a successful run — never by a browser. `Authorization: Bearer mtaenr_…` is a single-use, short-lived enrollment token bound to one server (id, and IP/hostname when minted with them). Body `{ id, hostname, agent_url, server_ip, ovh_vps_service?, ips?, agent_secret, smtp_password, webhook_secret }` over TLS. The app checks that agent_url uses the hostname and that it resolves to server_ip, proves the agent holds agent_secret with a signed /v1/health round-trip, consumes the token atomically and stores the server with its secrets sealed — no Fly secrets, no restart. A failed proof does not burn the token. Never echoes secrets. Rate-limited per client IP.","security":[],"responses":{"200":{"description":"`{ server: { id, hostname, agent_url, server_ip, status, reenrolled } }`"},"401":{"description":"Token invalid, used or expired."},"403":{"description":"Token minted for another server."},"409":{"description":"Server id is retired."},"422":{"description":"Invalid body, agent_url not pointing at server_ip, or the agent failed the signed health check."},"429":{"description":"Rate limited."}}}},"/api/v1/internal/mta/enroll-tokens":{"post":{"operationId":"createMtaEnrollToken","tags":["mta"],"summary":"Mint a one-time MTA enrollment token","description":"Body `{ server_id, server_ip?, hostname?, ovh_vps_service?, ttl_min? }` (ttl 1–1440, default 120). Returns the token ONCE (`{ token, id, server_id, expires_at, enroll_url }`); only its SHA-256 is stored. Hand it to `npm run mta:bootstrap -- --enroll` (MTA_ENROLL_TOKEN_FILE). Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"201":{"description":"`{ token, id, server_id, expires_at, enroll_url }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/mta/servers":{"get":{"operationId":"listMtaServers","tags":["mta"],"summary":"MTA server registry","description":"Env servers (MTA_SERVERS_JSON) merged with ENROLLED servers (mta_servers): id, source, status, hostname, agent URL, primary IP, OVH service, whether the agent is keyed; recent enrollment tokens (no hashes) and provisioning runs. Never returns a secret. Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ servers: [...], tokens: [...], provisions: [...], problems: [], notes: [] }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/internal/mta/servers/{id}/status":{"post":{"operationId":"setMtaServerStatus","tags":["mta"],"summary":"Drain, reactivate or retire an enrolled MTA server","description":"Body `{ status: 'active' | 'draining' | 'retired' }`. Draining keeps its domains and takes no new ones; retired leaves the registry (its webhook secret stops working) and cannot come back under the same id. Enrolled servers only. Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"`{ id, status }`"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"No enrolled server with that id."},"409":{"description":"The server is retired."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Server id.","schema":{"type":"string"}}]}},"/api/v1/internal/mta/servers/provision":{"post":{"operationId":"provisionMtaServer","tags":["mta"],"summary":"Provision a new MTA server end to end","description":"Body `{ server_id, ovh_vps_service? | vps_plan_code + vps_configuration?, extra_ips? (2–16, default 2), ip_plan_code?, ip_configuration?, ovh_subsidiary? }`. Starts the `mta_server_provision` job: (optional) OVH VPS + Additional IP orders → mta-<n>.<ops> A records → OVH rebuild (Ubuntu 24.04) with a first-boot script that downloads this server's bootstrap bundle and runs bootstrap.sh → the server enrolls itself → IP onboarding. Off unless MTA_PROVISION_ENABLED=true; the rebuild WIPES the given VPS and is refused for any VPS already in the registry; orders need OVH_VPS_ORDERS_ENABLED / OVH_ORDERS_ENABLED and their price caps. Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"202":{"description":"`{ server_id, step }` — progress in GET /api/v1/internal/mta/servers"},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"422":{"description":"Refused (provisioning off, bad plan, ordering off)."}}}},"/api/v1/internal/mta/bootstrap-bundle":{"get":{"operationId":"getMtaBootstrapBundle","tags":["mta"],"summary":"Bootstrap bundle for a provisioned MTA server","description":"Fetched by the first-boot script of a provisioned VPS. `Authorization: Bearer mtaenr_…` — a PROVISION token, unused and unexpired (downloading does not consume it; the same token enrolls the server). Only the server the token is bound to may fetch it (its IP). Returns `application/gzip`: infra/mta + this server's rendered config + bootstrap.env — no secrets, no DKIM keys. Rate-limited per client IP.","security":[],"responses":{"200":{"description":"The bundle (application/gzip)."},"401":{"description":"Token invalid, used, expired or not a provision token."},"403":{"description":"Requested from another IP than the server the token is for."},"429":{"description":"Rate limited."}}}},"/api/v1/internal/pool/import":{"post":{"operationId":"importInternalPoolMailboxes","tags":["pool"],"summary":"Import pool mailboxes from CSV","description":"Body `{ csv, dry_run?, region? }`. Same importer as `npm run pool:import-mailboxes`: creates domain + mailbox + sealed transport rows in the pool tenant. `dry_run` defaults to true (preview). `region` (CH|EU|US|WORLD) also registers each new domain as a campaign sender. Passwords are sealed before storage and never echoed: the response carries counts, created addresses/domains and per-row messages only. Body capped at ~1 MB (413). Operator only: session auth, admin role, and the operator tenant (403 otherwise).\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Import (or preview) result."},"400":{"description":"Missing/invalid csv, invalid region, or CSV missing required columns."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"413":{"description":"Body too large."}}}},"/api/v1/internal/marketprior/source-pass":{"post":{"operationId":"runInternalMarketPriorSourcePass","tags":["leads"],"summary":"Run one MarketPrior sourcing pass for a tenant","description":"Calls MarketPrior's candidates API (marketprior-v2/docs/COGNILEAD_CANDIDATES_API.md), runs each candidate through the legal-basis gate, email-candidate generation/verification, and the same intersect() gate every customer-supplied lead passes through (suppression, verification, 90-day recent-pitch dedup), then inserts sourced leads directly into the requested tenant. Requires tenant_id, target_product, and country in the body. Used by the operator dashboard / a scheduler to force a sourcing pass outside the normal cadence. Admin only.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Sourcing pass result — fetched/sourced/skipped/dropped/rejected counts."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tenant_id":{"type":"string","minLength":1},"target_product":{"type":"string","minLength":1},"country":{"type":"string","minLength":2,"maxLength":2},"region":{"type":"string"},"entity_type":{"type":"string","enum":["company","store"]},"min_intent_score":{"type":"number","minimum":0,"maximum":100},"vertical":{"type":"string"},"limit":{"type":"integer","minimum":1,"maximum":200}},"required":["tenant_id","target_product","country"],"additionalProperties":false}}}}}},"/api/v1/pool/request-mailboxes":{"post":{"operationId":"requestPoolMailboxes","tags":["pool"],"summary":"Request warmed mailboxes","description":"Reserves up to N warmed mailboxes from the shared pool for the calling tenant (always the caller's own tenant). Limits: the plan's total seat cap (included sender domains × 3 mailboxes by default); a shared-pool floor (reservations never take more than half of the pool by default); members' campaign bookings spend a 5-mailbox / 24h budget; persistent seats need the admin role AND an active paid plan. A request that fits only partly is granted partly — `assigned` < `requested`, with `limited_by` = tenant_cap | pool_floor | daily_budget | pool_supply. Member minimum.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Reserved mailboxes; `assigned` may be fewer than `requested` (see `limited_by`)."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"persistent seats without the admin role (`forbidden`) or without an active paid plan (`plan_required`)."},"409":{"description":"`tenant_mailbox_cap_reached` (plan seat cap full; body has max_assigned + assigned) or `pool_floor_reached` (nothing reservable without draining the shared pool)."},"429":{"description":"Daily campaign budget used up for a member (purpose=campaign)."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer","minimum":1,"maximum":100},"purpose":{"type":"string","enum":["campaign","persistent"]}},"required":["count"],"additionalProperties":false}}}}}},"/api/v1/pool/byo-warming-start":{"post":{"operationId":"startByoWarming","tags":["pool"],"summary":"Start bidirectional peer-warming for a BYO domain","description":"Registers a bring-your-own domain in domain_pool with state=warming, then creates BIDIRECTIONAL peer_pairs for each supplied tenant inbox address: a pool mailbox SENDS to the BYO inbox (pool_to_byo) AND the BYO inbox SENDS to a pool mailbox (byo_to_pool). The byo_to_pool direction is what builds the BYO domain real outbound SENDING reputation — the pool mailbox auto-replies. Supplying optional `smtp` creds enables the active BYO→pool send path; without them that direction warms via inbound correlation only (inbound-warm-only). Member minimum.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"201":{"description":"Domain registered and peer pairs created."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string","minLength":3},"daily_volume":{"type":"integer","minimum":1,"maximum":200},"addresses":{"type":"array","items":{"type":"string","format":"email"},"minItems":1,"maxItems":20},"smtp":{"type":"object","properties":{"host":{"type":"string","minLength":1},"port":{"type":"integer","minimum":1,"maximum":65535},"username":{"type":"string","minLength":1},"password":{"type":"string","minLength":1},"secure":{"type":"boolean"}},"required":["host","port","username","password"],"additionalProperties":false}},"required":["domain","addresses"],"additionalProperties":false}}}}}},"/api/v1/pool/my-mailboxes":{"get":{"operationId":"listMyPoolMailboxes","tags":["pool"],"summary":"List my pool mailboxes","description":"Returns every mailbox currently assigned to the calling tenant. Member minimum.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"List of assigned mailboxes."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/pool/release":{"post":{"operationId":"releasePoolMailboxes","tags":["pool"],"summary":"Release mailboxes back to the pool","description":"Voluntarily returns mailboxes to the pool by transitioning them to cooling_off with a 7-day timer. The tenant must own the assignments — releasing a mailbox owned by another tenant is rejected. Member minimum.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Mailboxes released to cooling_off."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mailbox_ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1}},"required":["mailbox_ids"],"additionalProperties":false}}}}}},"/api/v1/unsubscribe":{"post":{"operationId":"inboundUnsubscribe","tags":["unsubscribe"],"summary":"Inbound mailto unsubscribe","description":"Webhook target for mailbox-provider inbound mail (Postmark inbound / Cloudflare Email Workers / etc.) when a recipient uses the mailto branch of List-Unsubscribe. Token in the subject line is HMAC-verified before suppression. Always returns 200 — invalid tokens surface as `{ok: false}` so providers do not retry.","security":[],"responses":{"200":{"description":"Inbound mail processed (may be a no-op for invalid tokens)."},"400":{"description":"Malformed inbound payload."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string","minLength":1},"subject":{"type":"string","minLength":1}},"required":["from","subject"],"additionalProperties":false}}}}}},"/api/v1/webhooks":{"get":{"operationId":"listWebhooks","tags":["webhooks"],"summary":"List webhook endpoints","description":"Returns the calling tenant outbound webhook endpoints. The signing secret is returned only once at creation and is never included here.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"List of endpoints (without secrets)."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}},"post":{"operationId":"createWebhook","tags":["webhooks"],"summary":"Register a webhook endpoint","description":"Registers an https endpoint CogniLead POSTs events to (send.delivered, send.bounced, send.complained, send.replied, send.unsubscribed, reputation.degraded, domain.renewal_unfunded, privacy.erasure; or `*` for all). Each delivery is HMAC-SHA256 signed over `${timestamp}.${body}` with the returned secret (headers `Cognilead-Webhook-Timestamp` + `Cognilead-Webhook-Signature: sha256=…`). The secret is returned ONCE here. The URL must be absolute https and resolve to a public address — loopback, private, link-local/cloud-metadata and internal hosts are refused — and deliveries never follow redirects.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"201":{"description":"Endpoint created; secret returned ONCE."},"400":{"description":"`invalid_webhook`: the URL is not https, resolves to a private/loopback/link-local/internal address, or an event name is unknown."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}}},"required":["url"],"additionalProperties":false}}}}}},"/api/v1/webhooks/{id}":{"delete":{"operationId":"deleteWebhook","tags":["webhooks"],"summary":"Delete a webhook endpoint","description":"Removes an outbound webhook endpoint by id. Admin minimum.\n\n**Minimum role:** `admin`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Endpoint deleted."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"`not_found`: no endpoint with this id in the calling workspace."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook endpoint identifier.","schema":{"type":"string"}}]}},"/api/v1/customers":{"get":{"operationId":"listCustomers","tags":["customers"],"summary":"List the tenant's end-customers","description":"Lists the calling tenant's end-customers. No secrets are returned. Customer sub-keys are default-denied — an end-customer cannot enumerate its siblings.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Customer list."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}},"post":{"operationId":"createCustomer","tags":["customers"],"summary":"Provision an end-customer and mint a sub-key","description":"Provisions one of the tenant's end-customers and mints a customer sub-key scoped to it. The cleartext secret is returned ONCE. Idempotent on the customer — `customer_id` is derived from (tenant, email), so calling again returns the same customer with a fresh sub-key. The minted key is fixed at role `viewer` and is default-denied on every route except the customer-scoped usage/credits/topup surfaces.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Customer provisioned; sub-key secret returned once."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"label":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}]},"store_email":{"type":"boolean"}},"required":["email"],"additionalProperties":false}}}}}},"/api/v1/customers/{id}/usage":{"get":{"operationId":"getCustomerUsage","tags":["customers"],"summary":"Usage for one end-customer","description":"Rolled-up sends + LLM cost/tokens for a single end-customer over a window. A tenant-level caller may read any of its own customers; a customer sub-key may read only its own id (403 otherwise). Tenant isolation is enforced underneath by RLS.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Usage rollup for the customer."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"No such customer for this tenant."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Opaque customer identifier.","schema":{"type":"string"}},{"name":"window","in":"query","required":false,"description":"Look-back window in days (default 30).","schema":{"type":"integer"}}]}},"/api/v1/customers/{id}/credits":{"get":{"operationId":"getCustomerCredits","tags":["customers"],"summary":"Prepaid credit balance for one end-customer","description":"Returns the balance as the sum of `credit_ledger` deltas (top-ups positive, usage burns negative). Same ownership rule as the usage route: a customer sub-key may read only its own id.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Credit balance."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"No such customer for this tenant."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Opaque customer identifier.","schema":{"type":"string"}}]}},"/api/v1/customers/{id}/topup":{"post":{"operationId":"createCustomerTopup","tags":["customers"],"summary":"Top up an end-customer's credit balance","description":"Starts a Stripe Checkout to top up an end-customer's prepaid credit balance and returns `{ url }` to redirect the payer to. The credit is applied to `credit_ledger` by the `checkout.session.completed` webhook, not by this call. A customer sub-key may top up only its own balance.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Stripe Checkout URL."},"400":{"description":"Invalid request body or query parameters."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"404":{"description":"No such customer for this tenant."}},"parameters":[{"name":"id","in":"path","required":true,"description":"Opaque customer identifier.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"amount_usd":{"type":"number","minimum":1,"maximum":100000}},"required":["amount_usd"],"additionalProperties":false}}}}}},"/api/v1/consumption":{"get":{"operationId":"getConsumption","tags":["reseller"],"summary":"Fleet-wide reseller consumption","description":"Reseller-wide (global) phi-cloud consumption across ALL tenants and customers — the figure CogniLead reconciles against upstream. Operator only: session auth, admin role, and the operator tenant — any other workspace gets 403. An API key is rejected, because this exposes fleet-wide cost rather than tenant-scoped data.\n\n**Minimum role:** `admin`.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Global consumption totals."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."}}}},"/api/v1/monopea/usage":{"get":{"operationId":"getMonopeaUsage","tags":["reseller"],"summary":"Tenant usage from the brain","description":"Proxies the tenant's authoritative usage from the Monopea brain. Tenant-scoped — customer sub-keys are default-denied, so this is the tenant's aggregate view. Note the underlying figures come from a nightly rollup and can lag by up to ~24h.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Usage as reported by the brain."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"409":{"description":"Tenant is not provisioned on the brain yet."}},"parameters":[{"name":"window","in":"query","required":false,"description":"Look-back window in days (default 30).","schema":{"type":"integer"}}]}},"/api/v1/monopea/billing":{"get":{"operationId":"getMonopeaBilling","tags":["reseller"],"summary":"Tenant billing state from the brain","description":"Proxies the tenant's Monopea billing state — credit balance, pricing, budget, and top-up URL. Tenant-scoped.\n\n**Minimum role:** `viewer`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Billing state as reported by the brain."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"409":{"description":"Tenant is not provisioned on the brain yet."}}}},"/api/v1/monopea/embed-token":{"post":{"operationId":"createMonopeaEmbedToken","tags":["reseller"],"summary":"Mint an embed token for the hosted usage dashboard","description":"Mints a short-lived token for the hosted Monopea usage dashboard and returns an `embed_url` the browser can iframe with no key present. Tenant-scoped.\n\n**Minimum role:** `member`.","security":[{"BearerApiKey":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Embed token and URL."},"401":{"description":"Authentication required or invalid."},"403":{"description":"Authenticated but role does not permit the action."},"409":{"description":"Tenant is not provisioned on the brain yet."}}}}},"components":{"securitySchemes":{"BearerApiKey":{"type":"http","scheme":"bearer","bearerFormat":"pk_live_*","description":"Server-side API key minted at POST /api/v1/keys. Send as `Authorization: Bearer pk_live_…`. Tenant is pinned to the key."},"SessionCookie":{"type":"apiKey","in":"cookie","name":"sb-access-token","description":"Supabase SSR session cookie. Set by the dashboard sign-in flow; tenant is resolved through tenant_members."}}}}