# Havadis API — REST /v1

Keyed REST façade over Havadis content generation. Authenticate with `Authorization: Bearer havadis_<prefix>_<secret>`. Errors use a closed envelope with stable `code` values.

Machine-readable spec: https://api.gethavadis.co/api/v1/openapi.json
Node SDK: npm install @havadis/sdk (docs ship in the package under docs/)
Auth: Authorization: Bearer havadis_<prefix>_<secret> — mint in the dashboard, Settings → Developer. Server-side only.

## Endpoints

- `GET /api/v1/me` — Identity, effective scopes and limits of the calling key
  No scope required beyond a valid key.
- `GET /api/v1/credits` — Credit pool snapshot of the workspace owner's account
  Requires scope: `credits:read`.
- `GET /api/v1/brands` — Brands owned by the calling tenant
  Requires scope: `brands:read`.
- `GET /api/v1/brands/{brandId}` — One owned brand
  Requires scope: `brands:read`.
- `GET /api/v1/brands/{brandId}/contents` — Keyset-paged contents, drafts included
  Requires scope: `content:read`.
- `GET /api/v1/brands/{brandId}/contents/{idOrSlug}` — Full content (body + SEO/AEO/GEO), ETag/If-None-Match aware
  Requires scope: `content:read`.
- `POST /api/v1/brands/{brandId}/contents/{contentId}/publish` — Publish to the customer's own site (custom_website)
  Requires scope: `content:write`.
- `POST /api/v1/brands/{brandId}/contents/{contentId}/unpublish` — Take content off the custom_website channel
  Requires scope: `content:write`.
- `POST /api/v1/brands/{brandId}/jobs` — Queue a content generation job (async — poll jobs.get)
  Requires scope: `jobs:write`. Spends credits (all-or-nothing at execution). The `Idempotency-Key` header is REQUIRED; retries replay the stored result.
- `GET /api/v1/brands/{brandId}/jobs` — Keyset-paged jobs
  Requires scope: `jobs:read`.
- `POST /api/v1/brands/{brandId}/jobs/cost-preview` — Free per-model cost preview for a job shape
  Requires scope: `jobs:read`.
- `GET /api/v1/brands/{brandId}/jobs/{jobId}` — One job with per-platform state
  Requires scope: `jobs:read`.
- `POST /api/v1/brands/{brandId}/topics/suggest` — Queue topic discovery (async — results land in topics.list)
  Requires scope: `topics:write`. Spends credits (all-or-nothing at execution). The `Idempotency-Key` header is REQUIRED; retries replay the stored result.
- `GET /api/v1/brands/{brandId}/topics` — Topic suggestions for the brand
  Requires scope: `topics:read`.

## Rules that matter

- Errors: closed envelope `{ error: { type, code, message, ... }, request_id }` with frozen English codes; map codes, never messages.
- Credit-spending POSTs require an `Idempotency-Key` header; the server replays the stored result on retries for 24h.
- Lists are keyset-paged: pass `next_cursor` back verbatim; a malformed cursor is a 400, never page one.
- 429s (`rate_limit_exceeded`, `daily_spend_cap_exceeded`) carry `retry_after_seconds` and a `Retry-After` header — wait, do not hammer.
- `cover_image_url` values are stable re-signing routes; embed them as returned.
