Skip to main content

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"
}
StatusMeaning
200Token issued
401access_token header missing, expired, or invalid
401User not found or not associated with a company
400Invalid scope format
401appClientId 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"
}
StatusMeaning
200Token valid, identity returned
400token field missing from body
401Token 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.
ScopeGrants 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

MethodHow scopes are setGood for
Explicit (readScopes / writeScopes)Embedded in the JWT at issue timeOne-off scripts, agentic workflows
Marketplace (appClientId)Stored on the registered marketplace item, looked up at verify timeRegistered 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

StatusCodeMeaning
400Required field missing from request body
400INVALID_DATAScope has invalid format or unknown namespace
401INVALID_ACCESS_TOKENToken expired, signature mismatch, or wrong company
401UNAUTHORIZEDUser or app not found, company mismatch, or token revoked
403FORBIDDENToken valid but lacks required scope for this resource