/v1/projects*, authenticated with an
account API key (ck_acct_…) instead of a project key. Reach for it when your integration
needs to create and manage Craftkit projects itself — most commonly one project per customer
org in a multi-tenant backend — rather than rendering against a single project you set up by hand
in the dashboard.
If you only render against one fixed project, you don’t need this — a single
ck_live_…
project key is enough. See Server-to-server integration. Reach
for an account key only when your integration itself creates/lists/renames/deletes projects or
mints project keys programmatically.The credential
An account key looks likeck_acct_xxxxxxxxxxxxxxxxxxxxxxxx and is minted from the dashboard
(Account → API keys) — there is no endpoint that issues one. It is sent the same way as a
project key:
What an account key can do
Create a project
POST /v1/projectsList projects
GET /v1/projectsGet / rename / delete
GET, PATCH, DELETE /v1/projects/:idMint a project key
POST /v1/projects/:id/keysproject.userId must equal the
account resolved from the bearer token, and the project must not be soft-deleted — via a single
centralized ownership check reused by every route. A project id that belongs to a different
account behaves identically to one that doesn’t exist: 404, never 403, never a peek at the
other account’s data (OWASP API1:2023 — Broken Object Level Authorization).
An account key cannot render, read templates, poll renders, or reach signatures directly —
those still require the project’s own ck_live_… key, minted via the endpoint above.
Multi-tenant pattern: one project per customer
Hold one account key
Store
ck_acct_… once, server-side, for your whole integration — not per customer.Create a project per customer org (once)
POST /v1/projects with a name derived from the customer. Store the returned id /
slug against that customer in your own database.Mint and cache a project key for that customer (once)
POST /v1/projects/:id/keys. The plaintext ck_live_… key is returned exactly once — cache it
(e.g. in your existing getOrgApiKey / resolveApiKey-style lookup) keyed by customer id.Errors
/v1/projects* uses the same error envelope as the rest of the API.
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_json | Body is not valid JSON. |
| 422 | invalid_request | Body was valid JSON but failed schema validation (missing/empty/oversized name, etc.) — applies uniformly to POST /v1/projects, PATCH /v1/projects/:id, and POST /v1/projects/:id/keys. |
| 401 | unauthorized | Missing, invalid, revoked, or wrong-type (ck_live_…) bearer token. |
| 404 | not_found | Project id not owned by this account, or already soft-deleted. |
| 409 | conflict | POST /v1/projects only — a (userId, slug) unique-index collision after the automatic suffix-retry loop is exhausted. |
Next
Authentication
The three credential types and their scopes.
Server-to-server integration
The single-project render/poll playbook once you have a
ck_live_ key.