Key format
Every key is prefixed with environment and scoped to a single project:ndsk_test_ key in your
production env is a clear bug. Our SDKs and dashboards both surface the prefix
so it’s hard to confuse the two.
Where to get a key
Open Settings → API keys
From the dashboard at app.neuraldraft.io.
Click Create key
Pick a memorable name (e.g.
vercel-prod, lovable-preview,
claude-code-laptop). Future-you reading the audit log will thank present-you.Choose the scopes
Default is read+write across CMS, blog and social. Tighten to least
privilege when the key has a known job — for example, a static-site build
only needs
content:read and blog:read.Sending the key
A standard Bearer token in theAuthorization header. No query-param fallback,
no body field. HTTPS is required.
Scopes
Scopes are wildcard-prefixed. A key withcontent:* can do every action under
content; * is full access (use it carefully — only for trusted backend
services).
| Scope | What it covers |
|---|---|
* | Full access. Equivalent to all scopes below. |
project:read | Read project metadata, usage, key list. |
project:write | Update project (e.g. webhook URL, target languages). |
project:admin | Mint/revoke API keys, change billing. |
content:read | Read translation keys, components, pages. |
content:write | Create/update/delete translations and components. |
brand:read | Read brand context (voice, colors, fonts, audience). |
brand:write | Patch brand context, upload logos. |
images:read | Read media library and resolved image URLs. |
images:write | Upload, generate, replace images. |
blog:read | Read posts, categories, tags, translations. |
blog:write | Create/update/delete posts, schedule, publish. |
social:read | Read social posts and connected accounts. |
social:write | Create/update/delete social posts. |
social:publish | Publish a social post immediately to a connected platform. |
social:connect | Initiate OAuth flow, manage connected accounts. |
booking:read | Read services, availability, bookings. |
booking:write | Create/update services, availability, bookings. |
booking:admin | Update booking settings (timezone, policies, reminders). |
commerce:read | Read products, variants, categories, orders. |
commerce:write | Create/update/delete products, variants, categories, orders. |
commerce:admin | Refunds, Stripe Connect onboarding, manual orders. |
webhooks:read | List webhook endpoints and deliveries. |
webhooks:write | Create/update/delete webhook endpoints. |
jobs:read | Read job status (returned by all async ops). |
403 Forbidden with code: "forbidden" is returned when the key lacks the
required scope; the response includes the scope you’d need.
Common errors
| Status | code | When |
|---|---|---|
401 | unauthorized | Missing, malformed, expired or revoked key. |
403 | forbidden | Key is valid but lacks the required scope. |
402 | insufficient_credits | Project balance too low for the operation. Top up or upgrade. |
429 | rate_limited | Per-project window exhausted. See Retry-After. |
503 | service_unavailable | Backend dependency down. Retry with backoff. |
Best practices
One key per environment
One key per environment
Mint separate keys for production, staging, preview, and each developer
laptop. The audit log shows last-used timestamps per key — anomalies become
obvious.
Rotate quarterly
Rotate quarterly
Mint a new key, deploy with both old and new accepted (we accept multiple
valid keys per project), then revoke the old one once nothing 401s. The
dashboard has a one-click rotation flow that automates the overlap window.
Never commit a key
Never commit a key
Use environment variables. On Vercel/Netlify/Render, the encrypted env UI
is the right place. For local dev, a
.env file in .gitignore and
dotenv-style loading. Our CLI refuses to write a key to disk outside of
a recognised secret manager.Use least-privilege scopes
Use least-privilege scopes
A static-site builder needs
content:read and blog:read, nothing more.
A scheduler-only service needs social:write and social:publish. Keys
leak; small blast radius is the only mitigation that works at scale.Treat `ndsk_test_` as throwaway
Treat `ndsk_test_` as throwaway
Test keys are fine to share with contractors, paste into Lovable previews,
or include in a public GitHub issue when reproducing a bug. They have a
separate credit pool that resets monthly and never bills.
Watch the audit log
Watch the audit log
Every key has a
last_used_at and a per-IP usage stripe in the dashboard.
Suspicious activity = a chance to rotate before damage compounds. Suspect
leakage? Revoke immediately; it takes effect within 60 seconds globally.Revoking a key
A revoked key starts returning401 unauthorized within ~60 seconds globally.
Active jobs queued against the key continue to completion (they were
authorised at submit time); new requests fail.
Multiple keys per project
A project can hold up to 20 active keys. Use this for environment separation and friction-free rotation:| Name | Scopes | Where it lives |
|---|---|---|
prod-server | * (or write scopes) | Server-side, secret manager |
prod-builder | content:read, blog:read | CI build pipeline |
prod-mcp | * | Claude Code on dev laptops |
staging-server | * | Staging deployments |
lovable-preview | content:*, images:* | Lovable’s hosted preview env |
409 Conflict with code: "key_limit_reached" —
revoke an unused one first.
Central login (multi-workspace accounts)
If a single email is registered against more than one project (common for agencies and people running multiple sandboxes), the central login flow atapp.neuraldraft.io needs to know which
workspace to drop the user into. Two endpoints power this — both live on
the central host (app.<central_domain>), neither is a per-tenant API
surface, and neither uses an API key: they’re for the dashboard /
admin SPA, not for server-to-server traffic.
The v1 Project API documented above is unchanged — these endpoints are a
separate auth path the dashboard uses to issue Sanctum tokens. Skip this
section if you’re only consuming
/v1/*.GET /central/api/tenants-for-email
Returns every workspace the email is registered against. Always 200; the
response is constant-time across known/unknown emails to defeat
enumeration. An unknown email returns { tenants: [] }.
tenants.length > 1, render a workspace picker; otherwise log straight in.
POST /central/api/login
Email + password authentication that issues a Sanctum token. The optional
tenant_id disambiguates when the email is registered to multiple
workspaces — pass it whenever the picker has a selection so iteration
across candidates can’t pick the wrong one (e.g. password rotated on one
tenant but a stale row still points at another).
tenant_id is optional — if you omit it, the server iterates each candidate
workspace and uses the first one whose password verifies. Iteration is
case-insensitive on email. A wrong-password attempt always returns
422 Unprocessable Entity with auth.failed, regardless of whether the
email exists.
Where Sanctum and OAuth fit in
The v1 Project API documented here is API-key only. The separate Admin API — used by white-label dashboards and agencies managing multiple projects — uses scoped Sanctum tokens (tenants:read,
posts:write, …) and is documented in its own reference. If you’re building
your own admin UI on top of Neural Draft, that’s the auth model you want; for
everything else, stay here.