> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.jelliu.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.jelliu.co/_mcp/server.

# Authentication

Every request to `/api/*` is authenticated with a **workspace API key** sent as a Bearer token:

```http
GET /api/agents HTTP/1.1
Host: api.jelliu.co
Authorization: Bearer jl_0f9e8d7c6b5a4938271605f4e3d2c1b0a998877665544332211ff00ee00dd00
```

A key belongs to exactly one workspace. Every request made with it acts on that workspace, and rate limits are counted per workspace, not per key.

## Key format

| Part                                 | Value                                                 |
| ------------------------------------ | ----------------------------------------------------- |
| Prefix                               | `jl_`                                                 |
| Secret                               | 64 lowercase hexadecimal characters (32 random bytes) |
| Public prefix shown in the dashboard | `jl_` plus the first 8 hex characters                 |

The header must match `Bearer jl_` followed by exactly 64 characters from `0-9a-f`. Jelliu stores only a SHA-256 hash of the key, so the full value is shown **once**, when it is created, and cannot be retrieved later.

## Creating and revoking keys

Keys are managed in the dashboard under **Settings → API Keys**. Creating, listing and revoking keys is restricted to the workspace **owner**.

* **Expiry is optional.** A key created without an expiry date never expires. If you set one, it is honored exactly; it must be in the future.
* **Limit:** a workspace can hold up to **25 active keys**. Creating one more fails with `409` and code `PLAN_LIMIT_EXCEEDED`:
  `Maximum 25 active API keys per tenant. Revoke old keys before creating new ones.`
* **Revocation** takes effect right away on the instance that handles it. Validation results are cached for a few seconds, so allow up to about **10 seconds** for a revoked key to be refused everywhere.
* Keys also stop working when they expire or when their workspace is deleted.

API keys cannot manage credentials. Minting or revoking API keys, and rotating a webhook signing secret, require a signed-in human in the dashboard, even with a `full` key. An API key attempting it receives `403 FORBIDDEN`:
`This action requires a signed-in admin session — API keys cannot perform credential-management operations.`

## Scopes

Each key carries one or more scopes: `read`, `write` or `full`.

| Scope   | Read requests (`GET`, `HEAD`, `OPTIONS`) | Mutations (`POST`, `PUT`, `PATCH`, `DELETE`) | Admin-only routes |
| ------- | ---------------------------------------- | -------------------------------------------- | ----------------- |
| `read`  | Yes                                      | No                                           | No                |
| `write` | Yes                                      | Yes                                          | No                |
| `full`  | Yes                                      | Yes                                          | Yes               |

`write` implies read access. Scopes are enforced on every `/api` request by HTTP method, and again on routes that are restricted to particular workspace roles.

### What `full` means

A `full` key stands in for a workspace **administrator**. On routes that only admins, owners or billing users may call, Jelliu does not look up a role for API keys: a `full` key passes and any other key is refused. That includes, for example:

* creating, updating, deleting, activating and pausing campaigns;
* creating, updating and deleting outbound webhooks;
* deleting agents;
* the owner-only surfaces such as listing API keys, the audit log and plan changes.

Routes that members can use, such as creating and updating agents or adding contacts, accept a `write` key.

Use the least privilege that does the job. A reporting integration only needs `read`; an integration that adds contacts needs `write`. Reserve `full` for automations that manage campaigns or webhooks, and store those keys like an admin password.

## Error responses

### 401 Unauthorized

A key with the right shape that is unknown, revoked or expired:

```json
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or revoked API key"
  }
}
```

A missing `Authorization` header, or a bearer value that is not a well-formed `jl_` key:

```json
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication required — sign in with Clerk or provide a Bearer API key"
  }
}
```

### 403 Forbidden

A key whose scope does not cover the request:

```json
{
  "error": {
    "code": "FORBIDDEN",
    "message": "API key lacks the 'write' scope required for this operation"
  }
}
```

A `read` or `write` key calling an admin-only route:

```json
{
  "error": {
    "code": "FORBIDDEN",
    "message": "This operation requires an API key with the 'full' scope"
  }
}
```

A suspended workspace receives `403` with code `TENANT_SUSPENDED` on every request.

See [Errors](/errors) for the full error envelope.