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 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 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):
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):
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).
{
"readScopes": ["boards:*"],
"expirationSeconds": 86400
}
Response:
{
"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.
curl -X POST https://api.getaptly.com/api/app/verify \
-H "Content-Type: application/json" \
-d '{ "token": "<delegate-token>" }'
Response:
{
"userId": "abc123",
"email": "[email protected]",
"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.
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:
{
"userId": "abc123",
"email": "[email protected]",
"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.
# 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": "[email protected]" }'
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
// 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);
// 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.
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 |