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

# API Overview

> Generate content programmatically with the DraftLift API.

# DraftLift API

The DraftLift API gives you programmatic access to content generation, memories, templates, and content management. Available on Business and Agency plans.

## Base URL

```
https://draftliftai.com/api/v1
```

## Authentication

All requests require a Bearer token. Create an API key from **Settings > API Keys** in your dashboard.

```bash theme={null}
curl -H "Authorization: Bearer dl_live_your_key_here" \
  https://draftliftai.com/api/v1/templates
```

See [Authentication](/api-reference/authentication) for details on creating and managing API keys.

## Scopes

Every API key carries a set of scopes that control which operations it can perform. Scopes are picked when the key is created (in **Settings > API Keys**) and cannot be changed afterward — re-issue a new key to change scopes.

| Scope   | Grants                                                                                                                       |
| ------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `read`  | List and fetch resources (templates, memories, contents, references, voice profiles, usage). Always present.                 |
| `write` | Create, update, and delete resources. Required for `generate`, `repurpose`, `score`, and any POST/PUT/PATCH/DELETE endpoint. |

**Defaults.** New keys are issued with `['read', 'write']`. Uncheck **Write** at creation time to mint a read-only key — useful for analytics dashboards or audit tools that should never mutate data.

**Errors.** Calling a write endpoint with a read-only key returns `403` with a structured detail body:

```json theme={null}
{
  "error_code": "FORBIDDEN",
  "details": {
    "error_code": "insufficient_scope",
    "message": "This endpoint requires the 'write' scope.",
    "required_scope": "write",
    "current_scopes": ["read"],
    "upgrade_action": "Re-issue your API key at https://app.draftliftai.com/settings/api-keys"
  }
}
```

The CLI surfaces this as `InsufficientScopeError` (exit code `2`) with the upgrade URL on its own line.

## Workspace binding

API keys can optionally be bound to a single workspace at creation time. Binding scopes the key to that workspace for any workspace-aware operation (references, contents, conversions, knowledge graph, workspace listing).

| Key type                     | Workspace resolution                                                                                                                              |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Bound** (one workspace)    | All workspace-scoped requests auto-target the bound workspace. Passing `workspace_id` is optional; passing a *different* workspace returns `403`. |
| **Unbound** (all workspaces) | Workspace-scoped requests must include `workspace_id` (body, query, or path). The CLI's `--workspace` global flag plumbs this through.            |

**Solo users** (exactly one workspace) get their key auto-bound at creation — there's nothing to disambiguate.

**Mismatch error.** Calling an endpoint with a `workspace_id` that doesn't match the key's binding returns `403`:

```json theme={null}
{
  "error_code": "FORBIDDEN",
  "details": {
    "error_code": "workspace_mismatch",
    "message": "This API key is bound to a specific workspace.",
    "bound_workspace_id": "ws_abc123",
    "requested_workspace_id": "ws_def456"
  }
}
```

Account-level reads (entitlements, usage, profile, voice profiles) are user-scoped, not workspace-scoped, and ignore binding.

## What's not in the API

A few surfaces are intentionally web-session-only and reject API keys with `401`. These require interactive browser auth:

* **Billing mutations** — `POST /billing/checkout-session`, `/billing/portal-session`, `/billing/cancel-subscription`, `/billing/refund-request`. Stripe checkout and refund flows are interactive by design.
* **Account mutations** — `PUT /me/settings` and `PUT /learning-preferences`. Account creation is owned exclusively by Supabase Auth; other account-level changes belong in the dashboard.
* **API key management** — `POST/PUT/DELETE /api-keys`. Managing keys with a key would be a bootstrap footgun. The one exception is `GET /api-keys/current`, which lets a key introspect its own metadata (used by `dl whoami`).
* **Admin routes** — the entire `/admin/*` namespace stays JWT-only.
* **Workspace mutations** — workspace create/update/delete is web-only; reads (`GET /workspaces`, `GET /workspaces/:id`) are available via API key.

If you have a use case that needs one of these surfaces from a headless context, let us know — we'd rather understand the workflow than expose the endpoint by default.

## v1 stability commitment

The `/api/v1` surface follows strict semver:

* **Additive changes only.** New endpoints, new optional request fields, and new response fields can ship without notice.
* **No breaking changes.** Removing endpoints, removing response fields, renaming fields, tightening validation, or changing status codes for existing flows are breaking changes and will not happen on `/api/v1`.
* **Breaking changes ship under `/api/v2`.** When v2 mounts, v1 continues to work for a documented deprecation window.
* **Stability tags.** Endpoints are tagged `stable` or `beta` in the OpenAPI spec. Beta endpoints follow the same v1 contract but may iterate faster on response shape until promoted to stable. Internal-only endpoints (CLI device-flow auth, dev tools) are not documented and not part of the v1 contract.

## Quickstart: generate in one call

The generate endpoint is the core of the API. Pass a template and your direction — DraftLift handles everything else.

### 1. Get your API key

Create one at **Settings > API Keys**. Copy it immediately — it's only shown once.

### 2. Pick a template

<CodeGroup>
  ```bash cURL theme={null}
  curl -s https://draftliftai.com/api/v1/templates \
    -H "Authorization: Bearer dl_live_your_key_here"
  ```

  ```python Python theme={null}
  import requests

  API_KEY = "dl_live_your_key_here"
  BASE = "https://draftliftai.com/api/v1"
  headers = {"Authorization": f"Bearer {API_KEY}"}

  templates = requests.get(f"{BASE}/templates", headers=headers).json()
  for t in templates["items"]:
      print(f"{t['id']}: {t['name']} ({t['platform']})")
  ```
</CodeGroup>

Note the `id` of the template you want (e.g. a LinkedIn Post template).

### 3. Generate

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://draftliftai.com/api/v1/generate \
    -H "Authorization: Bearer dl_live_your_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "template_id": 1,
      "user_direction": "Write about why most B2B companies waste money on generic SEO content instead of opinion-driven thought leadership"
    }'
  ```

  ```python Python theme={null}
  result = requests.post(
      f"{BASE}/generate",
      headers=headers,
      json={
          "template_id": 1,
          "user_direction": "Write about why most B2B companies waste money on generic SEO content instead of opinion-driven thought leadership",
      },
  ).json()

  print(result["generated_content"])
  # Content ID is returned for downstream operations
  content_id = result["content_id"]
  ```

  ```typescript TypeScript theme={null}
  const result = await fetch(`${BASE}/generate`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({
      template_id: 1,
      user_direction:
        "Write about why most B2B companies waste money on generic SEO content instead of opinion-driven thought leadership",
    }),
  }).then((r) => r.json());

  console.log(result.generated_content);
  const contentId = result.content_id;
  ```
</CodeGroup>

No `content_id` needed — the API creates one automatically and returns it in the response.

### 4. Review and finalize

After editing, finalize the content to trigger learning analysis:

```bash theme={null}
curl -X POST https://draftliftai.com/api/v1/finalizeContent \
  -H "Authorization: Bearer dl_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "content_id": 789,
    "finalized_content": "Your edited final version here..."
  }'
```

## Headless agent workflow

The API is designed for AI agents operating headlessly. A typical loop:

```python theme={null}
# 1. Generate
result = requests.post(f"{BASE}/generate", headers=headers, json={
    "template_id": TEMPLATE_ID,
    "user_direction": direction,
    "memory_ids": [1, 5],
}).json()

content_id = result["content_id"]
generated = result["generated_content"]

# 2. Review / edit (your agent logic here)
final_content = agent_review_and_edit(generated)

# 3. Finalize
requests.post(f"{BASE}/finalizeContent", headers=headers, json={
    "content_id": content_id,
    "finalized_content": final_content,
})
```

Three calls: generate, review in your environment, finalize. No content creation step needed.

## Generate with context

Pass `memory_ids` to inject your voice and knowledge into the generation:

```bash theme={null}
curl -X POST https://draftliftai.com/api/v1/generate \
  -H "Authorization: Bearer dl_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": 1,
    "user_direction": "Write a LinkedIn post about content-led growth",
    "memory_ids": [1, 5, 12]
  }'
```

You can also pass `reference_material_ids` for source material the AI should draw from.

## Rate limits

| Plan     | Requests/min | Requests/hour |
| -------- | ------------ | ------------- |
| Business | 100          | 1,000         |
| Agency   | 300          | 5,000         |

Rate limit headers are included in every response:

* `X-RateLimit-Limit` — Maximum requests per window
* `X-RateLimit-Remaining` — Requests remaining
* `X-RateLimit-Reset` — Unix timestamp when the window resets

## Error handling

All errors return a `detail` field:

```json theme={null}
{
  "detail": "Description of the error"
}
```

| Status | Meaning                                        |
| ------ | ---------------------------------------------- |
| `401`  | Missing or invalid API key                     |
| `403`  | Your plan doesn't include API access           |
| `404`  | Resource not found                             |
| `422`  | Validation error (check request body)          |
| `429`  | Rate limit exceeded — see `Retry-After` header |
