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

# Server-to-server integration

> The complete playbook for a backend integration that auto-renders a PDF with no human at render time — and a coverage map for every requirement.

This is the reference for a **headless** integration: your backend holds one project API key
(`ck_live_…`), calls Craftkit server-to-server, and gets back a shareable PDF with no embed
session and no browser involved. It is written for flows like an offline-first mobile app that
reconnects at sign-off and renders a document on the server.

<Note>
  **One key is enough.** A single project `ck_live_` key can render, read templates, poll
  renders, and create share links for its project — with no per-org provisioning. See
  [Authentication](/guides/authentication).
</Note>

## The end-to-end flow

<Steps>
  <Step title="Confirm the template (once)">
    Author the `charter-handover` (or your) template in the dashboard and publish it. Note the
    **slug** and read its **manifest** with [`GET /v1/templates/:slug`](/api-reference/get-template)
    so your `data` keys bind. See [Verify before you render](/guides/verify-before-render).
  </Step>

  <Step title="Render server-to-server">
    `POST /v1/templates/:slug/render` with your `data` and an `Idempotency-Key` header. Craftkit
    validates against the manifest and enqueues the job. See
    [Render a template](/api-reference/render-template).
  </Step>

  <Step title="Get the result">
    Poll [`GET /v1/renders/:id`](/api-reference/get-render) until `succeeded`, or receive a
    [webhook](/guides/webhooks). Return `downloadUrl` to your client, or mint a durable
    [share link](/guides/sharing).
  </Step>
</Steps>

## Requirement coverage

Every capability a backend integration typically asks for, and where it is answered. **Available**
means it works against the current API today; **By design** means the behavior is intentional and
documented; **Roadmap** means it is not built yet and the workaround is linked.

| Need                                                    | Status                                                | Where                                                               |
| ------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------- |
| A stable, published template reachable by slug          | Available                                             | [Verify before you render](/guides/verify-before-render)            |
| Read a template's manifest before rendering (fail fast) | Available                                             | [`GET /v1/templates/:slug`](/api-reference/get-template)            |
| Bind scalar variables by dot-path (`booking.code`)      | Available                                             | [Variables & loops](/guides/variables-and-loops)                    |
| Iterate an array (`areas[]`) into a repeating table     | Available (pipeline-dependent)                        | [Render pipelines](/guides/render-pipelines)                        |
| Bind a signature / dynamic image                        | Available (pipeline-dependent)                        | [Images & signatures](/guides/images-and-signatures)                |
| One `ck_live_` key renders headlessly, no provisioning  | By design                                             | [Authentication](/guides/authentication)                            |
| Synchronous render (`options.sync`)                     | By design: **not honored** — poll instead             | [Render lifecycle](/guides/render-lifecycle)                        |
| `downloadUrl` durability / expiry                       | By design: permanent public URL (or private download) | [Render lifecycle](/guides/render-lifecycle)                        |
| Authoritative status enum                               | By design                                             | [Render lifecycle](/guides/render-lifecycle)                        |
| Re-fetch a render later (offline replay)                | Available                                             | [`GET /v1/renders/:id`](/api-reference/get-render)                  |
| Durable, revisitable share link                         | Available                                             | [Sharing](/guides/sharing)                                          |
| Idempotent renders (dedupe retries)                     | Available                                             | [Idempotency](/guides/idempotency)                                  |
| Render-complete webhook (drop polling)                  | Available (dashboard-configured)                      | [Webhooks](/guides/webhooks)                                        |
| Host + namespace for headless calls                     | By design                                             | [Hosts & environments](/guides/hosts-and-environments)              |
| Private (non-public-bucket) PDF download                | Available (partner-key stream)                        | [Download a render](/api-reference/download-render)                 |
| Programmatic template create / publish via API          | Available                                             | [Create a template](/api-reference/create-template)                 |
| Published typed server SDK                              | Roadmap (call the REST API directly)                  | [API reference](/api-reference/render-template)                     |
| Render-only / read-only scoped keys                     | Roadmap                                               | [Authentication](/guides/authentication#key-scope)                  |
| Presigned / expiring `downloadUrl`                      | Roadmap                                               | [Render lifecycle](/guides/render-lifecycle#downloadurl-durability) |

## Worked example

A complete server-to-server render, with idempotency and polling.

<CodeGroup>
  ```bash cURL theme={null}
  # 1. Verify the template exists and read its manifest.
  curl https://api.craftkit.dev/v1/templates/charter-handover \
    -H "Authorization: Bearer $CRAFTKIT_API_KEY"

  # 2. Render (idempotent on the booking + handover identity).
  curl -X POST https://api.craftkit.dev/v1/templates/charter-handover/render \
    -H "Authorization: Bearer $CRAFTKIT_API_KEY" \
    -H "Idempotency-Key: handover-BK-12345-checkout" \
    -H "Content-Type: application/json" \
    -d '{
      "data": {
        "booking": { "code": "BK-12345", "activityName": "Sunset Charter" },
        "handover": { "kind": "checkout", "date": "2026-06-04", "signedBy": "Jane Doe" },
        "areas": [
          { "areaKey": "hull", "condition": "damage", "severity": "cosmetic", "location": "port bow" },
          { "areaKey": "engine", "condition": "ok" }
        ]
      },
      "options": { "filename": "charter-handover-BK-12345.pdf" }
    }'
  # → 202 { "id": "…", "status": "queued", "pollUrl": "…/v1/renders/…", "downloadUrl": null }
  ```

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

  // 1. Enqueue (idempotent — retrying the same key returns the same render).
  const enqueue = await fetch(`${BASE}/v1/templates/charter-handover/render`, {
    method: 'POST',
    headers: { ...headers, 'Idempotency-Key': 'handover-BK-12345-checkout' },
    body: JSON.stringify({
      data: {
        booking: { code: 'BK-12345', activityName: 'Sunset Charter' },
        handover: { kind: 'checkout', date: '2026-06-04', signedBy: 'Jane Doe' },
        areas: [
          { areaKey: 'hull', condition: 'damage', severity: 'cosmetic', location: 'port bow' },
          { areaKey: 'engine', condition: 'ok' },
        ],
      },
      options: { filename: 'charter-handover-BK-12345.pdf' },
    }),
  });
  const { id } = await enqueue.json();

  // 2. Poll to a terminal state.
  async function waitForRender(renderId: string, timeoutMs = 30_000) {
    const deadline = Date.now() + timeoutMs;
    let delay = 250;
    while (Date.now() < deadline) {
      const r = await fetch(`${BASE}/v1/renders/${renderId}`, { headers });
      const render = await r.json();
      if (render.status === 'succeeded') return render.downloadUrl as string;
      if (render.status === 'failed') throw new Error(render.errorMessage ?? 'render failed');
      await new Promise((res) => setTimeout(res, delay));
      delay = Math.min(delay * 2, 5_000);
    }
    throw new Error('render timed out');
  }

  const downloadUrl = await waitForRender(id);
  ```
</CodeGroup>

<Warning>
  **`areas[]` is the substance of a handover — confirm the pipeline first.** Array iteration and
  dynamic images only render on the loop-capable pipeline. Read
  [Render pipelines](/guides/render-pipelines) before you commit a template, and note the
  [loop-key rule](/guides/variables-and-loops#the-loop-key-rule) — a loop key must be a
  dot-free top-level key, or the array silently validates to empty.
</Warning>

## Next

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/guides/authentication">
    What one `ck_live_` key can and cannot do.
  </Card>

  <Card title="Render pipelines" icon="layer-group" href="/guides/render-pipelines">
    Three pipelines, three capability sets — pick the right one.
  </Card>

  <Card title="Idempotency" icon="repeat" href="/guides/idempotency">
    Make offline-replay retries safe.
  </Card>

  <Card title="Render lifecycle" icon="timer" href="/guides/render-lifecycle">
    Sync, status enum, and download durability.
  </Card>
</CardGroup>
