> ## 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.

# Aptly SDK Reference

> Complete reference for the window.aptly object — properties, methods, config scoping, and dev testing URL parameters.

> **Deprecated board listing:** `GET /api/board/{boardId}` examples on this page remain supported, but new integrations should use the read-only `POST /api/board/{boardId}/list` with the same parameters in a JSON body. Card create/update POST is unchanged. See [Board segments](/board-segments).

The Aptly SDK is loaded via a single script tag and populates `window.aptly`. New to building embedded apps? Start with the [embedded apps guide](/building-embedded-apps-&-dashboards).

Related documentation:

* [Building Embedded Apps & Dashboards](/building-embedded-apps-&-dashboards) — full guide for embedded apps and dashboards
* [Embed App Actions](/embed-actions) — per-action payload reference and permission model
* [Delegate Tokens](/delegate-tokens) — raw token exchange API for server-side integrations
* [Field Types Reference](/field-types) — value formats for reading and writing card data
* [Rate Limits](/rate-limits) — request limits and 429 handling
* [Board Segments](/board-segments) — the `POST /api/board/{boardId}/list` card-listing endpoint behind `aptly.fetch`

## Canonical pattern

Always call `requestToken` with `{ version: 2 }`. It returns a result object — token on success, structured error on failure — so you can handle rate limits and other failures without a separate side channel.

```js theme={null}
async function init() {
  const { token, error } = await aptly.requestToken({ version: 2 });

  if (error?.code === 'RATE_LIMITED') {
    setTimeout(init, error.retryAfterMs);
    return;
  }
  if (!token) return;

  const res = await aptly.fetch('/board/' + aptly.config.BOARD_ID + '?page=0');
  const { records } = await res.json();
}
```

> **Note:** `requestToken()` called without arguments returns a bare `string | null` for backward compatibility. Prefer `{ version: 2 }` in all new code.

***

## API reference

| Property / method | Type | Description |
| - | - | - |
| `aptly.token` | `string \| null` | Current delegate token. `null` if not yet received or if the last token request failed. |
| `aptly.org` | `{ id, name }` | Logged-in user's organization |
| `aptly.user` | `{ id, email, firstName, lastName, phone, role, teams }` | Logged-in user. `role` is an array of role names assigned to the user in this org. `teams` is an array of team IDs the user belongs to. |
| `aptly.config` | `object` | Admin-declared config variables, fully merged across all active scopes. See [Config scoping](#config-scoping). |
| `aptly.board` | `{ id, schema } \| null` | Board context — only present when the app is embedded as a board tab. `schema` is the board's field schema. |
| `aptly.boards` | `{ leases, properties, units, deals, tickets, workOrders, screenings } \| null` | Pre-fetched board data (`{ id, name, schema, records, total }`) for boards the app declared. Undeclared keys and missing board types are `null`. See [Standard boards](#standard-boards). |
| `aptly.fetch(path, options?)` | `Promise<Response>` | Auth-aware fetch. Prepends the Aptly API base URL (`https://core-api.getaptly.com`). Automatically retries once on `401` with a fresh token. Returns a standard `Response`. Omit `/api` from paths. See [Fetching board data](#fetching-board-data) for pagination details. |
| `aptly.requestToken(opts?)` | `Promise<string \| null>` or `Promise<{ token, error }>` | Refresh the delegate token. Pass `{ version: 2 }` to get a result object with structured error handling. See [Rate limiting](#rate-limiting). Normally not needed — `aptly.fetch` handles token refresh automatically. |
| `aptly.startEmulation(token, opts?)` | `Promise<window.aptly>` | Verify a dev token against the Aptly API, resolve real user/org identity, and populate `window.aptly` as if Aptly had delivered context. `opts.config` overrides config values; `opts.org` overrides org fields; `opts.boards` simulates pre-fetched board data. Returns `window.aptly`. |
| `aptly.actions` | `string[]` | Action IDs the app is permitted to trigger (set by the admin in the app definition). Empty array = no actions allowed. |
| `aptly.action(actionId, payload?)` | `Promise<{ success, error? }>` | Trigger a named Aptly action. Resolves `{ success: false, error: 'Not permitted' }` if the action isn't in `aptly.actions`, or `{ success: false, error: 'timeout' }` after 3 s with no response. See [Embed App Actions](/embed-actions). |
| `aptly.openEmailComposer(opts?)` | `Promise<{ success, error? }>` | Open the email composer. `opts`: `{ to?, subject?, content?, composeMode?, aptletInstanceId?, applicationId? }`. `to` takes an address, an array of addresses, or an array of `{ value, label }` objects — email addresses when `composeMode` is `'email'` (the default), phone numbers when it is `'sms'`. Only fires if `'open-email-composer'` is in `aptly.actions`. |
| `aptly.openSMSComposer(opts?)` | `Promise<{ success, error? }>` | Open the SMS composer. Same fields as `openEmailComposer`, but `composeMode` defaults to `'sms'` and `to`/`content` are phone numbers/plain text. Requires `'open-sms-composer'` in `aptly.actions`. |
| `aptly.createCard(opts?)` | `Promise<{ success, error? }>` | Open new-card form. `opts`: `{ boardId, fields? }`. Requires `'create-card'` in `aptly.actions`. |
| `aptly.createTask(opts?)` | `Promise<{ success, error? }>` | Open task creation form. `opts`: `{ title?, dueAt?, priority?, activityLogType?, note?, assigneeId?, aptletInstanceId?, aptletUuid? }` (`priority` defaults to `'low'`; `activityLogType` defaults to `'todo'`). Requires `'create-task'` in `aptly.actions`. |
| `aptly.createContact(opts?)` | `Promise<{ success, error? }>` | Open contact creation modal. `opts`: `{ firstname?, lastname?, phone?, email?, company?, title? }`. Requires `'create-contact'` in `aptly.actions`. |
| `aptly.navigateToContact(opts)` | `Promise<{ success, error? }>` | Navigate to a contact record. `opts`: `{ contactId }`. Requires `'navigate-to-contact'` in `aptly.actions`. |
| `aptly.openCardPane(opts)` | `Promise<{ success, error? }>` | Open card in side panel. `opts`: `{ cardId }`. |
| `aptly.openCardView(opts)` | `Promise<{ success, error? }>` | Open card in fullscreen modal. `opts`: `{ cardId }`. |
| `aptly.startDialer(opts)` | `Promise<{ success, error? }>` | Open phone dialer. `opts`: `{ number, name? }` — `number` must be E.164 format. Requires `'start-dialer'` in `aptly.actions`. |
| `aptly.createEvent(opts?)` | `Promise<{ success, error? }>` | Open the calendar event editor. `opts`: `{ recipients?, date?, comments? }`. Only `recipients` (as `{ value, label }` objects) is currently applied by the editor; `date`/`comments` are accepted but not read. Requires `'create-event'` in `aptly.actions`. |
| `aptly.openConversation(opts)` | `Promise<{ success, error? }>` | Open an email/SMS conversation thread in a floating panel. `opts`: `{ streamId, title? }` — `streamId` must be a non-empty string, otherwise the call resolves `success: false`. Requires `'open-conversation'` in `aptly.actions`. |

***

<h2 id="fetching-board-data">
  Fetching board data
</h2>

`aptly.fetch` prepends the Aptly API base URL and handles auth automatically. Pass the path starting from `/board/` — omit `/api`.

```js theme={null}
const res  = await aptly.fetch('/board/MY_BOARD_UUID?page=0');
const { data, count, page, pageSize } = await res.json();
// data    — array of card objects for this page
// count   — total matching cards across all pages
// page    — the page number you requested
// pageSize — number of cards per page (default 20, max 1000)
```

### Pagination

<Callout type="warning">
  Pages are **zero-based**. The first page is `page=0`, the second is `page=1`, and so on. The `page` parameter is required — omitting it returns a 400 error.
</Callout>

```js theme={null}
// ✅ Correct — first page
const res = await aptly.fetch('/board/lease?page=0&pageSize=100');

// ❌ Wrong — skips the first 100 records, returns empty if fewer than 100 exist
const res = await aptly.fetch('/board/lease?page=1&pageSize=100');
```

To page through all records:

```js theme={null}
async function fetchAllCards(boardId) {
  const pageSize = 100;
  let page = 0;
  let allCards = [];

  while (true) {
    const res = await aptly.fetch(`/board/${boardId}?page=${page}&pageSize=${pageSize}`);
    const { data, count } = await res.json();
    allCards = allCards.concat(data);
    if (allCards.length >= count) break;
    page++;
  }

  return allCards;
}
```

### Query parameters

| Param | Required | Description |
| - | - | - |
| `page` | Yes | Zero-based page number (0–9999) |
| `pageSize` | No | Cards per page. Default `20`, max `1000` |
| `updatedAtMin` | No | ISO timestamp — only return cards updated after this time |
| `includeArchived` | No | Set to `true` to include archived cards. Default excludes them |
| `assignee` | No | Filter by assignee user ID |
| `contactEmail` | No | Filter cards linked to this contact email |
| `keyTerm` | No | Full-text search on card title |

### Schema endpoint

To get the field definitions for a board (key → label → type mapping):

```js theme={null}
const fields = await aptly.fetch('/schema/MY_BOARD_UUID').then(r => r.json());
// [ { key: 'stage', label: 'Stage', type: 'stage' }, ... ]
```

The schema endpoint requires no additional parameters and returns an array directly (not wrapped in `{ data }`).

***

## Config scoping

`aptly.config` always contains the **fully merged** configuration for the current context — app devs just read `aptly.config.YOUR_KEY` without knowing which scope a value came from.

Admins set a `contextScope` on each app that controls how config values are stored:

| Scope | Where values are stored | Use case |
| - | - | - |
| `company` (default) | One record per org per app | All users and boards share the same config |
| `board` | One record per board per app | Different config per board — e.g. each board uses its own data source ID |
| `user` | One record per user per app | Personal settings that don't affect teammates |
| `all` | All three levels simultaneously | Maximum flexibility — user settings override board settings override company settings |

**Merge priority:** `user` > `board` > `company`. If the same key is set at multiple levels, the narrowest scope wins.

```js theme={null}
// Same code regardless of what scope the admin chose
const boardId = aptly.config.LEASES_BOARD_ID;
```

**Configuring board-scoped apps:** Users configure board-scoped apps directly in the board view settings panel, where the board context is known. Company and user-scoped apps are configured in the app store.

***

<h2 id="rate-limiting">
  Rate limiting
</h2>

Token requests are rate-limited **per browser connection** (not per user account), so opening the same app in multiple tabs does not share a quota. The default limit is **60 token requests per 60 seconds** per connection.

When the limit is exceeded, `requestToken({ version: 2 })` returns `{ token: null, error }` where `error` is:

```js theme={null}
{
  code: 'RATE_LIMITED',
  message: 'Rate limited. Retry in 42 seconds.',
  retryAfterMs: 42000   // milliseconds until the bucket resets
}
```

### Handling rate limit errors

Use `{ version: 2 }` and check the `error` field:

```js theme={null}
async function loadData() {
  const { token, error } = await aptly.requestToken({ version: 2 });

  if (error?.code === 'RATE_LIMITED') {
    console.warn(error.message);
    setTimeout(loadData, error.retryAfterMs ?? 60000);
    return;
  }
  if (!token) {
    console.error('Could not obtain token');
    return;
  }

  const res = await aptly.fetch('/boards/leases/records');
  const data = await res.json();
  // ...
}
```

`aptly.fetch` does not automatically retry on rate limit errors — it only retries once on `401`. Your app is responsible for scheduling retries when `error.code === 'RATE_LIMITED'`.

***

<h2 id="url-params">
  URL params (Option C dev testing)
</h2>

Append any of these to your app's URL to simulate Aptly context with no code changes:

| Param | Sets |
| - | - |
| `aptly_token` | `aptly.token` |
| `aptly_config_KEY` | `aptly.config.KEY` (any key name) |
| `aptly_org_id` | `aptly.org.id` |
| `aptly_org_name` | `aptly.org.name` |
| `aptly_user_id` | `aptly.user.id` |
| `aptly_user_email` | `aptly.user.email` |
| `aptly_user_first_name` | `aptly.user.firstName` |
| `aptly_user_last_name` | `aptly.user.lastName` |

Example:

```
https://your-app.repl.co?aptly_token=MY_TOKEN&aptly_config_BOARD_ID=lease&aptly_org_name=Acme
```

When running inside Aptly, URL params are ignored — the real postMessage context takes priority.

***

<h2 id="standard-boards">
  Standard boards
</h2>

`aptly.boards` delivers pre-fetched schema and records for standard Aptly board types — no fetches required. The app admin selects which boards to pre-load in the marketplace admin panel. Only declared boards are populated; undeclared keys are `null`.

### Board data shape

Each non-null entry is a `BoardData` object:

```ts theme={null}
type BoardData = {
  id:      string;                         // board UUID ('lease', 'location', etc.)
  name:    string;                         // display name ('Leases')
  schema:  { key: string; label: string; type: string }[];  // field definitions
  records: object[];                       // first 50 records, keyed by schema field keys
  total:   number;                         // total record count (for pagination)
}
```

### Usage

```js theme={null}
// Schema and records are ready immediately — no fetch needed
const leases = aptly.boards.leases;
if (leases) {
  console.log(leases.name);        // 'Leases'
  console.log(leases.schema);      // [{ key: 'name', label: 'Name', type: 'text' }, ...]
  console.log(leases.records);     // [{ _id: '...', name: 'Unit 1A', stage: 'Active' }, ...]
  console.log(leases.total);       // 847 — fetch more pages if total > 50
}
```

### Board keys

| Key | Board type |
| - | - |
| `aptly.boards.leases` | Leases |
| `aptly.boards.properties` | Properties / Buildings |
| `aptly.boards.units` | Units |
| `aptly.boards.deals` | Deals |
| `aptly.boards.tickets` | Tickets / Work Orders |
| `aptly.boards.workOrders` | Work Orders |
| `aptly.boards.screenings` | Screenings |

A key is `null` if the app didn't declare that board, or if the company doesn't have it. Always guard:

```js theme={null}
if (aptly.boards.leases) { /* safe to use */ }
```

### Fetching more records

The first 50 records are pre-loaded. If `total > 50`, use `aptly.fetch` to page through the rest:

```js theme={null}
const { id, records, total } = aptly.boards.leases;
// records already has page 0 — fetch additional pages if needed
if (total > records.length) {
  const more = await aptly.fetch('/board/' + id + '?page=1').then(r => r.json());
}
```

### Dev mode

`aptly.boards` is `null` by default in `startEmulation` and `APTLY_DEV` (no live Aptly session to deliver data). Pass a `boards` override to simulate it locally. You can use either the full rich object or a plain UUID string (the SDK normalizes it):

```html theme={null}
<script>
  window.APTLY_DEV = {
    token: 'MY_DEV_TOKEN',
    boards: {
      // Full object — closest to production
      leases: { id: 'lease', name: 'Leases', schema: [{ key: 'name', label: 'Name', type: 'text' }], records: [], total: 0 },
      // Shorthand UUID string — SDK wraps it automatically
      properties: 'location'
    }
  };
</script>
```

Or via `startEmulation`:

```js theme={null}
aptly.startEmulation('MY_DEV_TOKEN', {
  boards: { leases: 'lease' }
});
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.