Skip to main content
Most public API requests authenticate with a project API key sent as a bearer token:
Keys are stored as SHA-256 hashes, scoped to one project, and minted in the dashboard (Project → API keys) or programmatically via POST /v1/projects/:id/keys. They are environment-specific: a key created on localhost only authenticates against http://localhost:3000; a production key only authenticates against https://api.craftkit.dev.

Credential types

Craftkit has three distinct bearer/secret credential types. They are not interchangeable — each is validated against its own table (or, for the admin key, a direct env-var comparison), and a credential of one type is always rejected wherever another type is expected.
These are strictly disjoint, but not by the same mechanism in both directions. An account key on any non-/v1/projects* endpoint, a project key on /v1/projects*, or either one on /v1/admin/provision all return 401 — but the two /v1/projects* validators get there differently:
  • Account-key validation short-circuits on prefix. It only accepts tokens starting with ck_acct_; anything else (including a well-formed ck_live_ key) is rejected with 401 before any database lookup happens.
  • Project-key validation has no prefix check. It hashes whatever token it’s given and looks it up in the api_key table. A ck_acct_ token is simply never found there — the 401 comes from table separation, not from inspecting the token’s shape.
Both paths land on 401, but only one of them is a prefix short-circuit; the other relies on the account key’s hash never having been written to the project-key table.
For the account key specifically — creating and managing projects, and minting project keys for them programmatically — see Account-key auth.

One key, full project access

A project key is full-access within its project. There is no per-key scope, role, or permission column today — every valid ck_live_ key for a project can do all of the following for that project, with no per-org provisioning:

Render

POST /v1/templates/:slug/render

Read templates

GET /v1/templates and GET /v1/templates/:slug

Poll renders

GET /v1/renders/:id

Create shares

POST /v1/renders/:id/shares
The same key is also accepted as the embed partner secret, so it can reach partner-key embed endpoints such as GET /v1/embed/builder/templates (list) and GET /v1/embed/renders/:id/download (private stream).
For a headless render integration you need exactly one ck_live_ key, and the template must live in the project that key belongs to. No admin key, no embed session, no provisioning.

401 vs 404

These two are easy to confuse when a render “fails”:
  • 401 unauthorized — the key is missing, malformed, revoked, or from the wrong environment.
  • 404 template_not_found — the key authenticated fine, but no template with that slug exists in that key’s project. Usually means the key resolves to a different project than you expect, or the template was never created/published there.

The admin key is separate

CRAFTKIT_ADMIN_KEY is a single global environment secret that gates only the multi-tenant provisioning endpoints (POST/PATCH /v1/admin/provision). It is compared directly against the env value and never consults the API-key table — so a ck_live_ key sent to /v1/admin/provision returns 401. You do not need the admin key for headless rendering.

Key scope

Today a project key is all-or-nothing within its project, which means the key you embed in a backend service can also create and publish templates. There is no render-only or read-only scoped key yet (roadmap). Until then:
  • Mint a dedicated key per integration (rendering, embed, inbound webhooks each separate) so you can revoke one without disrupting the others.
  • Store keys in environment variables, never in client code or logs.

Key management

Project keys can be minted from the dashboard (Project → API keys) or programmatically with POST /v1/projects/:id/keys, using the project’s account key. There is no rotation endpoint — rotate by minting a new key, deploying, verifying traffic, then revoking the old one. Account keys (ck_acct_…) are dashboard-only (Account → API keys) — there is no endpoint that issues one, by design, so provisioning your account’s own top-level credential always requires a human in the dashboard once. Revocation is immediate with no grace period for both credential types — a revoked key stops authenticating on its very next use.