The flow
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):
expirationSeconds to override the default 5-minute TTL. Maximum is 7 days (604800).
| 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 plusappClientIdandappTitle. - Scoped dev tokens — issued by a GA admin via the Dev Tokens panel, without an
appClientId. Returns user identity withappClientId: null. Revoked tokens return401.
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.
| 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 asAuthorization: DelegateToken <token> on any supported route. The token’s scopes must cover the resource.
DelegateToken as the Authorization scheme — not Bearer.
Scopes
Scopes follow the formatresource: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
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.
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: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 |