Skip to main content
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:

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 on window.aptly. Your page then calls the Aptly REST API directly with the token. Three architectural rules apply to every embedded app:
  1. 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.
  2. 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.
  3. 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>:
The SDK is served from app.getaptly.com only. After it loads, window.aptly is available:
Detect SDK readiness by polling for 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:
  1. If aptly.token is already populated, initialize immediately.
  2. Otherwise call aptly.requestToken({ version: 2 }) and handle the result.
  3. Fall back to a bounded poll for aptly.token only when running against an SDK build that does not expose requestToken.
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:
Handle failures immediately rather than waiting for a timeout:
  • RATE_LIMITED — retry once after error.retryAfterMs milliseconds.
  • Any other code — display error.message and error.code to the user with instructions (typically: enable Needs Aptly Context and confirm the app is authorized).
  • A null token from a v1-only SDK build — treat as a failure with the same guidance.
Wrap the request in a hard timeout (8 seconds is a reasonable default) so an unanswered postMessage cannot stall the page indefinitely. The complete worked example below shows the full boot sequence.

Reading board data

Resolve fields from the schema — never hardcode field IDs

Fetch GET /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:
Built-in fields use semantic keys (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 429 response, retry once after the Retry-After header (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", or 1.
  • Dates may be ISO (2026-05-15) or MM-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 the Authorization: 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 global error 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 addEventListener in script blocks. Inline handler attributes (onclick=, onchange=, and similar) are not preserved.
  • Build DOM with createElement and textContent, not concatenated HTML strings assigned to innerHTML. 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, write returns the total rather than returns <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 \uXXXX escapes inside string literals.
  • Keep lines under 200 characters.
Skip the refresh button by default. Pages fetch live data on every load and embedded tabs reload when opened. Add a manual refresh control only when users keep the tab open for extended periods.

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 from aptly.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. 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:
  • startEmulation resolves even when verification fails; a failed verify leaves aptly.user unpopulated. After it resolves, check aptly.user && aptly.user.id and treat an unpopulated identity as an invalid token.
  • When the page runs genuinely embedded in Aptly, real postMessage context takes priority over emulation.
Do not hardcode dev tokens in page source. Dev tokens are live credentials. The recommended pattern is a “Developer mode” toggle that expands into a paste field: the operator pastes a token at runtime, the app verifies it via 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:
See the SDK Reference for the full parameter list.

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 a Promise of { success, error? } and is a no-op outside Aptly. Actions must be enabled per embed by an admin:
See Embed App Actions for the full list, payloads, and setup.

Complete worked example: board overview

The page below runs on any board with no custom-field requirements — it uses only the built-in stage 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:
  1. POST https://core-api.getaptly.com/api/platform/user-token with the logged-in user’s session JWT in the access_token header and a body of { "readScopes": ["boards:*"], "writeScopes": [] } — or { "appClientId": "..." } for registered marketplace apps. Optional expirationSeconds up to 604800 (7 days); the default expiry is 5 minutes.
  2. Verify identity when needed: POST /api/app/verify with { "token": "<token>" }. No API key is required.
  3. Call any supported route with Authorization: DelegateToken <token>.
Scopes follow 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

  1. No calls to AI model APIs anywhere in the page.
  2. No snapshot data embedded in the source; all data fetched at load.
  3. SDK script tag points at app.getaptly.com.
  4. Boot order: SDK readiness poll (bounded) -> existing token, else requestToken({ version: 2 }) with fail-fast error handling and one RATE_LIMITED retry -> bounded token poll only as a v1 fallback.
  5. Schema fetched before card data; fields resolved by label and key with variants; missing-field errors list the board’s actual fields.
  6. Pagination uses the { data, count } envelope, caps pages, pauses between pages, and retries once on 429.
  7. Every error path renders in the UI; global error and unhandledrejection handlers installed.
  8. No inline event handler attributes; all listeners attached with addEventListener.
  9. DOM built with createElement and textContent; no HTML-string innerHTML.
  10. Source is pure ASCII, has no < + letter sequences outside quoted strings, and keeps lines under 200 characters.
  11. No tokens or other credentials in the source; developer mode (if present) accepts a pasted token at runtime.
  12. The target board has API access enabled, and the tab has Needs Aptly Context turned on.