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

# Delegate Tokens

> Issue short-lived JWTs so an embedded app, server-side script, or AI agent can call the Aptly API on behalf of a logged-in user — without exposing a raw API key.

Delegate tokens are short-lived JWTs (default 5-minute expiry) that represent a specific logged-in Aptly user. An embedded app or agent receives the token and uses it directly for API calls. Tokens are scoped — they only grant access to the resources you declare at issue time.

If you're building a Replit app or iframe widget, the [Aptly SDK](/delegate-auth) handles all of this automatically. Come here if you need the raw token exchange API for a server-side integration, a custom auth flow, or an AI agent.

***

## The flow

```
Your server                         Aptly API
──────────────────────────────────────────────────────────────
 1. User is logged in to Aptly.
    Their session JWT is available server-side.

    POST /api/platform/user-token   ───────────────────────►
    access_token: <user-session-jwt>
    body: { readScopes, writeScopes }
                                    ◄───────────────────────
                                    { token, expiration }

 2. Deliver token to embedded app / agent (postMessage,
    response body, env var, etc).

 3. Embedded app / agent uses the token:

    A. Verify identity (no API key needed):
       POST /api/app/verify ─────────────────────────────►
       body: { token }
                             ◄────────────────────────────
                             { userId, email, firstName,
                               lastName, companyId,
                               appClientId, appTitle }

    B. Call the API directly:
       GET /api/board/:boardId ───────────────────────────►
       Authorization: DelegateToken <token>
                                   ◄─────────────────────
                                   board data
```

***

## Issue a delegate token

**`POST /api/platform/user-token`**

Call this from your server. Requires the logged-in user's session JWT in the `access_token` header.

**Option A — explicit scopes** (no marketplace app registration needed):

```bash theme={null}
curl -X POST https://api.getaptly.com/api/platform/user-token \
  -H "access_token: <user-session-jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "readScopes":  ["boards:*", "contacts:*"],
    "writeScopes": ["boards:*"]
  }'
```

**Option B — registered marketplace app** (scopes come from the marketplace item):

```bash theme={null}
curl -X POST https://api.getaptly.com/api/platform/user-token \
  -H "access_token: <user-session-jwt>" \
  -H "Content-Type: application/json" \
  -d '{ "appClientId": "your-app-client-id" }'
```

**Optional: custom expiry**

Both options accept `expirationSeconds` to override the default 5-minute TTL. Maximum is 7 days (604800).

```json theme={null}
{
  "readScopes": ["boards:*"],
  "expirationSeconds": 86400
}
```

**Response:**

```json theme={null}
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expiration": "2025-01-01T12:05:00.000Z"
}
```

| Status | Meaning                                            |
| ------ | -------------------------------------------------- |
| 200    | Token issued                                       |
| 401    | `access_token` header missing, expired, or invalid |
| 401    | User not found or not associated with a company    |
| 400    | Invalid scope format                               |
| 401    | `appClientId` not found for this company           |

***

## Verify the token

Use this to confirm who the token belongs to before trusting an incoming request.

### Option A — keyless verify

**`POST /api/app/verify`**

No API key needed. Works for two token types:

* **Marketplace tokens** — issued with `appClientId`. Returns user identity plus `appClientId` and `appTitle`.
* **Scoped dev tokens** — issued by a GA admin via the Dev Tokens panel, without an `appClientId`. Returns user identity with `appClientId: null`. Revoked tokens return `401`.

```bash theme={null}
curl -X POST https://api.getaptly.com/api/app/verify \
  -H "Content-Type: application/json" \
  -d '{ "token": "<delegate-token>" }'
```

Response:

```json theme={null}
{
  "userId":     "abc123",
  "email":      "user@example.com",
  "firstName":  "Jane",
  "lastName":   "Smith",
  "companyId":  "company456",
  "appClientId": "your-app-client-id",
  "appTitle":   "My Plugin"
}
```

### Option B — verify with board API key

**`POST /api/board/verify-user`**

Works for any delegate token. Requires a board API key from the same company.

```bash theme={null}
curl -X POST https://api.getaptly.com/api/board/verify-user \
  -H "x-token: <board-api-key>" \
  -H "Content-Type: application/json" \
  -d '{ "token": "<delegate-token>" }'
```

Response:

```json theme={null}
{
  "userId":    "abc123",
  "email":     "user@example.com",
  "firstName": "Jane",
  "lastName":  "Smith",
  "companyId": "company456"
}
```

| Status | Meaning                                                                      |
| ------ | ---------------------------------------------------------------------------- |
| 200    | Token valid, identity returned                                               |
| 400    | `token` field missing from body                                              |
| 401    | Token expired, signature invalid, revoked, or issued for a different company |

***

## Call the API with a delegate token

Pass the token as `Authorization: DelegateToken <token>` on any supported route. The token's scopes must cover the resource.

```bash theme={null}
# Read a board
curl https://api.getaptly.com/api/board/<boardId> \
  -H "Authorization: DelegateToken <delegate-token>"

# Create a card
curl -X POST https://api.getaptly.com/api/board/<boardId>/card \
  -H "Authorization: DelegateToken <delegate-token>" \
  -H "Content-Type: application/json" \
  -d '{ "name": "New card" }'

# Look up a contact
curl -X POST https://api.getaptly.com/api/contacts/by-email \
  -H "Authorization: DelegateToken <delegate-token>" \
  -H "Content-Type: application/json" \
  -d '{ "email": "contact@example.com" }'
```

**Always use `DelegateToken` as the Authorization scheme — not `Bearer`.**

***

## Scopes

Scopes follow the format `resource:qualifier`.

| Scope              | Grants access to                        |
| ------------------ | --------------------------------------- |
| `boards:*`         | All board routes for the user's company |
| `boards:<boardId>` | A single specific board                 |
| `contacts:*`       | Contact lookup and verification routes  |
| `knowledge:*`      | Knowledge base routes                   |
| `email:*`          | Email draft and send routes             |

**`readScopes`** — GET requests and read operations.
**`writeScopes`** — POST/PUT/DELETE requests and write operations.

A missing scope returns `403 Forbidden`. No scopes are granted by default — omitted scopes are denied.

### Marketplace vs explicit scopes

| Method                                  | How scopes are set                                                  | Good for                                                                       |
| --------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Explicit (`readScopes` / `writeScopes`) | Embedded in the JWT at issue time                                   | One-off scripts, agentic workflows                                             |
| Marketplace (`appClientId`)             | Stored on the registered marketplace item, looked up at verify time | Registered plugins where you want to update granted scopes without redeploying |

***

## Server-side example

```javascript theme={null}
// server.js
import express from "express";
const app = express();
app.use(express.json());

const APTLY_API = "https://api.getaptly.com";

app.post("/delegate-token", async (req, res) => {
  const { accessToken } = req.body;
  if (!accessToken) return res.status(400).json({ error: "accessToken required" });

  const response = await fetch(`${APTLY_API}/api/platform/user-token`, {
    method: "POST",
    headers: { "access_token": accessToken, "Content-Type": "application/json" },
    body: JSON.stringify({ readScopes: ["boards:*", "contacts:*"], writeScopes: ["boards:*"] }),
  });

  if (!response.ok) return res.status(401).json({ error: "Could not issue delegate token" });

  const { token, expiration } = await response.json();
  res.json({ delegateToken: token, expiration });
});

app.listen(3000);
```

```javascript theme={null}
// client.js
const APTLY_API = "https://api.getaptly.com";

const { delegateToken } = await fetch("/delegate-token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ accessToken: userSessionJwt }),
}).then(r => r.json());

const board = await fetch(`${APTLY_API}/api/board/${boardId}`, {
  headers: { Authorization: `DelegateToken ${delegateToken}` },
}).then(r => r.json());
```

***

## Iframe embed pattern (without the SDK)

If you can't use the Aptly SDK script tag, you can implement the postMessage handshake manually. The parent Aptly window responds to `{ type: 'aptly-token-request' }` with a token and context object.

```javascript theme={null}
const APTLY_API = "https://api.getaptly.com";
let aptlyToken = null;

function requestToken() {
  return new Promise((resolve) => {
    function handler(e) {
      if (e.data?.type === "aptly-delegate-token") {
        window.removeEventListener("message", handler);
        aptlyToken = e.data.token;
        resolve(aptlyToken);
      }
    }
    window.addEventListener("message", handler);
    window.parent.postMessage({ type: "aptly-token-request" }, "*");
  });
}

requestToken().then(init);

async function apiFetch(path, options = {}, retry = true) {
  const res = await fetch(`${APTLY_API}${path}`, {
    ...options,
    headers: { Authorization: `DelegateToken ${aptlyToken}`, ...options.headers },
  });
  if (res.status === 401 && retry) {
    await requestToken();
    return apiFetch(path, options, false);
  }
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}
```

**Register the listener before calling `postMessage`** — the parent may respond synchronously. The listener removes itself after the first matching message to avoid leaks.

**Retry once, not in a loop** — a second `401` after a fresh token means the session is actually expired or the scope is wrong. One retry distinguishes stale tokens from real auth failures.

***

## For AI agents

When Aptly embeds a generated app or AI agent into an iframe context, the system prompt will include a block declaring what scopes are available:

```
A delegate token is available in this embedding context. Granted scopes:
- `contacts:*` read
- `knowledge:*` read
- `boards:*` read
```

If you see this block, use `requestToken()` from the iframe embed pattern above to receive the token, then use `apiFetch` for all API calls. The scope list is authoritative — calling a route outside those scopes returns `403 Forbidden`. Check whether a scope appears under read or write access before attempting mutations.

***

## Error reference

| Status | Code                   | Meaning                                                   |
| ------ | ---------------------- | --------------------------------------------------------- |
| 400    | —                      | Required field missing from request body                  |
| 400    | `INVALID_DATA`         | Scope has invalid format or unknown namespace             |
| 401    | `INVALID_ACCESS_TOKEN` | Token expired, signature mismatch, or wrong company       |
| 401    | `UNAUTHORIZED`         | User or app not found, company mismatch, or token revoked |
| 403    | `FORBIDDEN`            | Token valid but lacks required scope for this resource    |
