> ## Documentation Index
> Fetch the complete documentation index at: https://docs.craftkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Account-key auth & the Projects API

> Hold one account key, manage every project under it, and mint per-project keys programmatically — the self-serve alternative to a single shared ck_live_ key.

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.

<Note>
  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](/guides/headless-integration). Reach
  for an account key only when your integration itself creates/lists/renames/deletes projects or
  mints project keys programmatically.
</Note>

## 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:

```http theme={null}
Authorization: Bearer ck_acct_xxxxxxxxxxxxxxxxxxxxxxxx
```

<Warning>
  **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](/guides/authentication#credential-types) in Authentication.
</Warning>

## What an account key can do

<CardGroup cols={2}>
  <Card title="Create a project" icon="plus" href="/api-reference/create-project">
    `POST /v1/projects`
  </Card>

  <Card title="List projects" icon="list" href="/api-reference/list-projects">
    `GET /v1/projects`
  </Card>

  <Card title="Get / rename / delete" icon="pen" href="/api-reference/get-project">
    `GET`, `PATCH`, `DELETE /v1/projects/:id`
  </Card>

  <Card title="Mint a project key" icon="key" href="/api-reference/create-project-key">
    `POST /v1/projects/:id/keys`
  </Card>
</CardGroup>

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

<Steps>
  <Step title="Hold one account key">
    Store `ck_acct_…` once, server-side, for your whole integration — not per customer.
  </Step>

  <Step title="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.

    <Warning>
      **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.
    </Warning>
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

<CodeGroup>
  ```bash cURL theme={null}
  # 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" } } }'
  ```

  ```ts TypeScript theme={null}
  const BASE = 'https://api.craftkit.dev';
  const acctHeaders = {
    Authorization: `Bearer ${process.env.CRAFTKIT_ACCOUNT_KEY}`,
    'Content-Type': 'application/json',
  };

  async function provisionCustomerProject(customerName: string) {
    const project = await fetch(`${BASE}/v1/projects`, {
      method: 'POST',
      headers: acctHeaders,
      body: JSON.stringify({ name: customerName }),
    }).then((r) => r.json());

    const minted = await fetch(`${BASE}/v1/projects/${project.id}/keys`, {
      method: 'POST',
      headers: acctHeaders,
      body: JSON.stringify({ name: 'Render service (prod)' }),
    }).then((r) => r.json());

    // Persist minted.key against this customer now — it is never returned again.
    return { projectId: project.id, slug: project.slug, apiKey: minted.key as string };
  }
  ```
</CodeGroup>

## Errors

`/v1/projects*` uses the same [error envelope](/api-reference/errors) 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

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/guides/authentication">
    The three credential types and their scopes.
  </Card>

  <Card title="Server-to-server integration" icon="server" href="/guides/headless-integration">
    The single-project render/poll playbook once you have a `ck_live_` key.
  </Card>
</CardGroup>
