Skip to main content
This is the reference for the account-scoped surface: /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 like ck_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:
Authorization: Bearer ck_acct_xxxxxxxxxxxxxxxxxxxxxxxx
Account and project keys are strictly disjoint credential systems. An account key (ck_acct_…) is rejected with 401 on every non-/v1/projects* endpoint (templates, renders, signatures, embed). A project key (ck_live_…/ck_test_…) is rejected with 401 on every /v1/projects* endpoint. Neither validator ever queries the other credential’s table. See the credential types in Authentication.

What an account key can do

Create a project

POST /v1/projects

List projects

GET /v1/projects

Get / rename / delete

GET, PATCH, DELETE /v1/projects/:id

Mint a project key

POST /v1/projects/:id/keys
Every one of these endpoints resolves ownership the same way — project.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

1

Hold one account key

Store ck_acct_… once, server-side, for your whole integration — not per customer.
2

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.
This endpoint is not idempotent. There is no idempotency key today, so a retried call with the same name (a timeout, a double-click, a retried job) creates a second project with a numeric-suffixed slug (acme-corp-1, acme-corp-2, …) rather than returning the original. Check your own store for an existing project before calling POST /v1/projects for a customer you may have already provisioned. An Idempotency-Key header may be added in a future release — until then, dedupe on your side.
3

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.
4

Use the cached project key for everything else

Render, read templates, poll renders, and send signatures for that customer with their cached ck_live_… key — the account key is not needed again until you provision the next customer.
# 1. One project per customer org. NOT idempotent — a repeat call with the
#    same derived name creates a second project with a suffixed slug, not an
#    error. Check your own store first if you might have already provisioned
#    this customer.
curl -X POST https://api.craftkit.dev/v1/projects \
  -H "Authorization: Bearer $CRAFTKIT_ACCOUNT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Acme Corp" }'
# → 201 { "id": "…", "slug": "acme-corp", "name": "Acme Corp" }

# 2. Mint a project key scoped to that project.
curl -X POST https://api.craftkit.dev/v1/projects/<id>/keys \
  -H "Authorization: Bearer $CRAFTKIT_ACCOUNT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Render service (prod)" }'
# → 201 { "id": "…", "prefix": "ck_live_aBcDe", "key": "ck_live_…" }  — store `key` now.

# 3. Render as that customer, from then on, with their cached ck_live_ key.
curl -X POST https://api.craftkit.dev/v1/templates/invoice/render \
  -H "Authorization: Bearer <cached ck_live_ key>" \
  -H "Content-Type: application/json" \
  -d '{ "data": { "customer": { "name": "Jane Doe" } } }'

Errors

/v1/projects* uses the same error envelope as the rest of the API.
StatuscodeMeaning
400invalid_jsonBody is not valid JSON.
422invalid_requestBody 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.
401unauthorizedMissing, invalid, revoked, or wrong-type (ck_live_…) bearer token.
404not_foundProject id not owned by this account, or already soft-deleted.
409conflictPOST /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.