Add the Aptly SDK to a custom dashboard, internal tool, or any iframe to get delegate auth, user identity, and config variables — then read and write Aptly data directly from the page. This guide is written for developers and for AI agents generating apps on a user’s behalf.Related documentation:
- SDK Reference — full
window.aptlyAPI, config scoping, URL params - Delegate Tokens — raw token exchange API for server-side integrations
- Embed App Actions — trigger Aptly UI interactions from an embedded app
- Field Types Reference — value formats for reading and writing card data
- Rate Limits — request limits and 429 handling
- Documentation index — machine-readable index of all pages
How embedded apps work
When your page runs inside Aptly (as a board tab or dashboard panel), Aptly delivers a short-lived delegate token and context — the logged-in user, their organization, and admin-declared config — to your page via postMessage. The SDK handles that handshake and exposes everything onwindow.aptly. Your page then calls the Aptly REST API
directly with the token.
Three architectural rules apply to every embedded app:
- The page calls the Aptly API at runtime. Do not call AI model APIs (Anthropic, OpenAI, or any other) from the page. Browser-side model calls require credentials that are only injected inside the model vendor’s own products; on a hosted page they fail with authentication and CORS errors. If an AI agent generates the page, the agent’s involvement ends at generation time.
- Fetch data fresh on every load. Never embed a snapshot of queried data in the page source. A snapshot renders correctly but is frozen at generation time, and the discrepancy is invisible to users until the numbers drift.
- Compute deterministically in the page. Filtering, aggregation, and report logic belong in plain JavaScript, not in a runtime model call. Client-side compute is instant, free, and produces the same result every time.
Quick start
Add one script tag to your HTML<head>:
app.getaptly.com only. After it loads, window.aptly is
available:
window.aptly with a bounded attempt count.
Do not attach an onerror handler to the script tag for this purpose: some sandboxed
environments fire onerror even when the script subsequently loads, which produces
false-positive failures.
Enable the SDK for your app
Open the board view settings for the tab that contains your app (or the dashboard panel) and toggle on Needs Aptly Context. This tells Aptly to deliver a delegate token and context to your app on each load. Without it, the postMessage handshake never completes and the page receives no token.Requesting the delegate token
On boot, follow this order:- If
aptly.tokenis already populated, initialize immediately. - Otherwise call
aptly.requestToken({ version: 2 })and handle the result. - Fall back to a bounded poll for
aptly.tokenonly when running against an SDK build that does not exposerequestToken.
requestToken never rejects. The default (v1) form resolves the token string, or
null on failure, with no further detail. The version-2 form resolves a result object:
RATE_LIMITED— retry once aftererror.retryAfterMsmilliseconds.- Any other code — display
error.messageanderror.codeto the user with instructions (typically: enable Needs Aptly Context and confirm the app is authorized). - A
nulltoken from a v1-only SDK build — treat as a failure with the same guidance.
Reading board data
Resolve fields from the schema — never hardcode field IDs
FetchGET /api/schema/{boardId} before reading or writing card data, and resolve the
fields your app needs at runtime. Match candidates against both the field label and
the field key, case-insensitively, with a list of accepted variants per field:
name, stage, dueAt); custom fields use
generated IDs; some boards expose camelCase keys with different display labels.
Matching both label and key handles every case and makes the app portable across
boards and companies. When a required field is missing, surface an error that lists the
fields the board actually has — this turns a configuration mismatch into something the
user can fix without developer help.
Paginate with the response envelope
GET /api/board/{boardId}?page=N&pageSize=M returns:
count is the board total. Use it for an exact stop condition and progress display.
Request pageSize=1000 (the maximum) so most boards load in one or two requests.
Respect the rate limit
The API allows 60 requests per minute per connection. For multi-page loads:- pause about 1 second between page requests,
- on a
429response, retry once after theRetry-Afterheader (or 5 seconds if the header is absent), and show a “retrying” message in the loading state, - cap every pagination loop at a hard maximum page count.
Normalize field value formats
Value formats differ between reading and writing, and across boards:- Money fields are returned as
{ amount, currency }from list endpoints but are written as plain numbers. Accept both when reading. - Checkbox values may arrive as
true,"true","Yes", or1. - Dates may be ISO (
2026-05-15) orMM-DD-YYYY. Parse both. - Treat unrecognized field types as plain strings. The Field Types Reference lists the write formats.
API error reference
All API calls use theAuthorization: DelegateToken <token> scheme — not Bearer —
against https://core-api.getaptly.com (aptly.fetch sets both automatically).
Notes: API access is enabled per board in the board’s settings, and
GET /api/boards returns only boards that have it enabled.
Building a reliable UI
Embedded apps are used by people who will not open browser developer tools. Build accordingly: Every error renders on screen. Each failure path — a missing field, an expired token, a rate limit, an unexpected exception — must appear in a visible error element with instructions for resolving it. Register globalerror and unhandledrejection
handlers so unanticipated exceptions also land in the UI. Do not rely on console.*
output for anything a user might need to see.
Every loop is bounded. Polls and pagination loops need a maximum attempt count, and
the timeout message should state it. Unbounded loops hang the page or hammer the API
when something unexpected happens.
Follow the hosted-page source requirements. Pages uploaded to Aptly’s page hosting
pass through an HTML transform. The following constraints keep source intact through
that transform (and are good practice everywhere):
- Attach all event handlers with
addEventListenerin script blocks. Inline handler attributes (onclick=,onchange=, and similar) are not preserved. - Build DOM with
createElementandtextContent, not concatenated HTML strings assigned toinnerHTML. This also avoids HTML-injection issues. - Do not place a
<character directly followed by a letter anywhere outside a quoted string — including inside comments (for example, writereturns the totalrather thanreturns <total>). Such sequences can be interpreted as markup. - Keep source pure ASCII. Replace em dashes, ellipses, smart quotes, and arrows with
ASCII equivalents, or use
\uXXXXescapes inside string literals. - Keep lines under 200 characters.
Config variables
Config variables let users configure your app with friendly pickers — board selectors, text fields, toggles — rather than pasting raw IDs. Admins declare them in the app store admin panel; users fill them in during install. Read them fromaptly.config
with a sensible fallback:
aptly.config is the fully merged configuration for the current context. If the app
supports board-level or user-level config, Aptly merges scopes before delivering
context (priority: user over board over company) — your code always reads one flat
object. See Config scoping.
Testing your app
Embedded context only exists inside Aptly, so local development uses one of three emulation options.Option A — startEmulation with a dev token (recommended)
An Aptly admin generates a long-lived developer token in Global Admin -> [Company] ->
Dev Tokens. Call startEmulation after the SDK loads:
startEmulation verifies the token against the Aptly API to resolve real user and org
identity, then populates window.aptly exactly as if Aptly had delivered the context.
Two behaviors to account for:
startEmulationresolves even when verification fails; a failed verify leavesaptly.userunpopulated. After it resolves, checkaptly.user && aptly.user.idand treat an unpopulated identity as an invalid token.- When the page runs genuinely embedded in Aptly, real postMessage context takes priority over emulation.
startEmulation, and on
success stores it in localStorage so dev mode survives reloads (cleared when the
toggle is turned off). Persist this state in localStorage, not the URL — when
embedded, the iframe URL belongs to Aptly’s tab configuration. A page built this way
ships zero secrets and the toggle can remain in any build.
Option B — window.APTLY_DEV block
Set values directly before the SDK tag — useful for mocking config without a token.
Remove before deploy:
Option C — URL params
No code changes needed; nothing to remove before deploy:Where testing works — and where it does not
Test in a regular browser tab (with one of the options above) or embedded in Aptly with Needs Aptly Context enabled. AI assistant sandboxes (for example, the Claude.ai artifact preview) enforce a Content Security Policy that blocks the SDK script from loading; iframes inside those sandboxes inherit the same policy. Pages that depend on the Aptly SDK cannot be previewed there — open the file in a normal browser tab instead.Triggering Aptly UI from your app
The SDK exposes named methods that open dialogs in the parent Aptly window — the phone dialer, email composer, card creation, contact navigation, and more. Each returns aPromise of { success, error? } and is a no-op outside Aptly. Actions must be
enabled per embed by an admin:
Complete worked example: board overview
The page below runs on any board with no custom-field requirements — it uses only the built-instage field and card basics. The top section is a wrapping grid of boxes
with the card count per stage; selecting a box filters the table below to that stage
(selecting it again, or “All cards”, clears the filter). Each table row demonstrates
embed actions: clicking the row opens the card in the board side pane, the expand icon
opens the card in a fullscreen view, the email icon looks up the card’s
linked contacts via the REST API and opens the composer with their email addresses as
recipients (to) and the card name as subject, and the phone icon opens the dialer
with the first linked phone number found. Action failures (not embedded,
not granted, timeout) render as on-screen messages. Every pattern in this guide is
applied: the boot sequence with version-2 token requests, schema-first field
resolution, rate-limited pagination, on-screen errors, transform-safe source, and
admin-granted UI actions. Use it as a starting template and extend it with
board-specific fields via resolveFields.
Using delegate tokens without the SDK
Server-side scripts, custom auth flows, and AI agents can use the raw token exchange directly:POST https://core-api.getaptly.com/api/platform/user-tokenwith the logged-in user’s session JWT in theaccess_tokenheader and a body of{ "readScopes": ["boards:*"], "writeScopes": [] }— or{ "appClientId": "..." }for registered marketplace apps. OptionalexpirationSecondsup to 604800 (7 days); the default expiry is 5 minutes.- Verify identity when needed:
POST /api/app/verifywith{ "token": "<token>" }. No API key is required. - Call any supported route with
Authorization: DelegateToken <token>.
resource:qualifier (boards:*, boards:<boardId>, contacts:*,
knowledge:*, email:*). Omitted scopes are denied; a missing scope returns 403.
Request the narrowest scope the app needs — a read-only dashboard needs only
readScopes: ["boards:<boardId>"].
Full details: Delegate Tokens.
Pre-deployment checklist
- No calls to AI model APIs anywhere in the page.
- No snapshot data embedded in the source; all data fetched at load.
- SDK script tag points at
app.getaptly.com. - Boot order: SDK readiness poll (bounded) -> existing token, else
requestToken({ version: 2 })with fail-fast error handling and oneRATE_LIMITEDretry -> bounded token poll only as a v1 fallback. - Schema fetched before card data; fields resolved by label and key with variants; missing-field errors list the board’s actual fields.
- Pagination uses the
{ data, count }envelope, caps pages, pauses between pages, and retries once on429. - Every error path renders in the UI; global
errorandunhandledrejectionhandlers installed. - No inline event handler attributes; all listeners attached with
addEventListener. - DOM built with
createElementandtextContent; no HTML-stringinnerHTML. - Source is pure ASCII, has no
<+ letter sequences outside quoted strings, and keeps lines under 200 characters. - No tokens or other credentials in the source; developer mode (if present) accepts a pasted token at runtime.
- The target board has API access enabled, and the tab has Needs Aptly Context turned on.