Typed JavaScript/TypeScript SDK for the Air API. See the developer docs for API reference and guides.
npm install @air/api-sdk
# or
yarn add @air/api-sdk
# or
bun add @air/api-sdkimport { AirApi } from "@air/api-sdk";
const air = new AirApi({
apiKey: "your-api-key",
workspaceId: "your-workspace-id",
});
// List boards
const page = await air.boards.list();
console.log(page.data);Instead of passing options directly, you can set environment variables:
export AIR_API_KEY=your-api-key
export AIR_WORKSPACE_ID=your-workspace-idconst air = new AirApi(); // reads from envconst air = new AirApi({
apiKey: "your-api-key", // or AIR_API_KEY env var
workspaceId: "your-workspace-id", // or AIR_WORKSPACE_ID env var
baseURL: "https://api.air.inc/v1", // default
maxRetries: 3, // default, with exponential backoff
timeout: 60_000, // default, in milliseconds
defaultHeaders: {
// optional, sent with every request
"user-agent": "my-app/1.0",
"x-custom-header": "value",
},
});Use defaultHeaders to attach headers to every request. This is useful for overriding the default user-agent (air-api-sdk/<version>) or passing tracking headers so the backend can identify your integration:
const air = new AirApi({
apiKey: "your-api-key",
workspaceId: "your-workspace-id",
defaultHeaders: {
"user-agent": "my-app/1.0",
"x-air-client-source": "my-integration",
},
});Headers are merged in order of precedence (last wins):
- SDK defaults (
x-api-key,x-air-workspace-id,user-agent: air-api-sdk/<version>) defaultHeadersfrom the constructor- Per-request
headerson individual API calls
// List boards
const page = await air.boards.list({ limit: 10 });
// Filter by name or parent
const filtered = await air.boards.list({ name: "My Board", parentBoardId: "board-id" });
// CRUD
const board = await air.boards.create({ title: "New Board", description: "Optional" });
const fetched = await air.boards.get(board.id);
await air.boards.update(board.id, { title: "Renamed" });
await air.boards.delete(board.id);
// Sub-boards
const child = await air.boards.create({ title: "Child", parentBoardId: board.id });
// Board assets
await air.boards.addAssets(board.id, { assetIds: ["asset-id-1", "asset-id-2"] });
await air.boards.removeAsset(board.id, "asset-id-1");
// Board custom fields
await air.boards.setCustomField(board.id, "custom-field-id", { value: "hello" });
await air.boards.setCustomField(board.id, "custom-field-id", { value: null }); // clear
// Guest management
const guest = await air.boards.addGuest(board.id, {
email: "guest@example.com",
roleId: "role-id",
});
const guests = await air.boards.listGuests(board.id);
const filtered = await air.boards.listGuests(board.id, { email: "guest@example.com" });
await air.boards.updateGuest(board.id, guest.id, { roleId: "new-role-id" });
await air.boards.removeGuest(board.id, guest.id);// List assets (supports filtering by board, tags, custom fields, search, date range)
const page = await air.assets.list({ parentBoardId: "board-id", limit: 20 });
const searched = await air.assets.list({ search: "logo" });
// Get and delete
const asset = await air.assets.get("asset-id");
await air.assets.delete("asset-id");
// Custom fields on assets
await air.assets.setCustomField("asset-id", "cf-id", { value: "text value" });
await air.assets.setCustomField("asset-id", "cf-id", { values: [{ id: "value-id" }] }); // select fields
// List boards an asset belongs to
const boards = await air.assets.listBoards("asset-id");const { data: versions } = await air.assets.listVersions("asset-id");
const version = await air.assets.getVersion("asset-id", "version-id");
await air.assets.updateVersion("asset-id", "version-id", { title: "New title" });
// Download URL
const { url } = await air.assets.getVersionDownloadUrl("asset-id", "version-id");
// Version tags
await air.assets.addVersionTag("asset-id", "version-id", { id: "tag-id" });
await air.assets.removeVersionTag("asset-id", "version-id", "tag-id");const page = await air.tags.list();
const tag = await air.tags.create({ name: "My Tag" });
const fetched = await air.tags.get(tag.id);
await air.tags.update(tag.id, { name: "Renamed Tag" });
await air.tags.delete(tag.id);// List and CRUD
const page = await air.customFields.list();
const cf = await air.customFields.create({
name: "Status",
type: "single-select", // 'single-select' | 'multi-select' | 'plain-text' | 'date'
values: [{ name: "Active" }, { name: "Archived" }],
});
await air.customFields.update(cf.id, { name: "Project Status" });
await air.customFields.delete(cf.id);
// Manage select field values
const value = await air.customFields.createValue(cf.id, { name: "In Review" });
await air.customFields.updateValue(cf.id, value.id, { name: "Under Review" });
await air.customFields.deleteValue(cf.id, value.id);const roles = await air.roles.list({ type: "guest" });
// Returns: [{ id, name, description, billable, type }]// High-level upload — from file path
const result = await air.uploads.uploadFile(
{ filePath: "./photo.png" },
{ parentBoardId: "board-id" },
);
console.log(result.assetId, result.versionId);
// From buffer
const result = await air.uploads.uploadFile(
{ buffer: myBuffer, fileName: "photo", ext: "png", mime: "image/png" },
{ parentBoardId: "board-id" },
);
// With progress tracking
await air.uploads.uploadFile(
{ filePath: "./video.mp4" },
{
parentBoardId: "board-id",
onProgress: ({ percentage, uploadedBytes, totalBytes }) => {
console.log(`${percentage}% (${uploadedBytes}/${totalBytes})`);
},
},
);
// With tags and custom fields
await air.uploads.uploadFile(
{ filePath: "./doc.pdf" },
{
parentBoardId: "board-id",
tags: [{ id: "tag-id" }],
customFields: [{ id: "cf-id", value: "some value" }],
},
);Files over 5 GB are automatically uploaded using multipart upload.
For low-level control, use air.uploads.create() directly to get a presigned URL.
// Import from URL
const imp = await air.imports.create({
sourceUrl: "https://example.com/image.png",
parentBoardId: "board-id",
title: "Imported Image",
});
console.log(imp.id, imp.assetId);
// Check import status
const status = await air.imports.getStatus(imp.id);
console.log(status.status); // 'pending' | 'inProgress' | 'succeeded' | 'failed'List methods return a PagePromise that supports two patterns:
for await (const asset of air.assets.list({ limit: 50 })) {
console.log(asset.id);
// automatically fetches subsequent pages
}const page = await air.assets.list({ limit: 50 });
console.log(page.data); // current page items
console.log(page.total); // total count (when available)
console.log(page.pagination); // { hasMore: boolean, cursor: string | null }
if (page.hasNextPage()) {
const next = await page.getNextPage();
}All API errors extend APIError with status, body, and headers properties. Specific error classes are thrown based on HTTP status:
| Status | Error class |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 403 | PermissionError |
| 404 | NotFoundError |
| 429 | RateLimitError |
| 500+ | InternalServerError |
Network failures throw ConnectionError, and timeouts throw TimeoutError.
import { NotFoundError, RateLimitError, APIError } from "@air/api-sdk";
try {
await air.assets.get("non-existent-id");
} catch (err) {
if (err instanceof NotFoundError) {
console.log("Asset not found");
} else if (err instanceof RateLimitError) {
console.log(`Rate limited, retry after ${err.retryAfter}s`);
} else if (err instanceof APIError) {
console.log(err.status, err.message);
}
}Retryable errors (408, 429, 500, 502, 503, 504) are automatically retried with exponential backoff up to maxRetries times.
- Node.js >= 18 (or Bun, or any npm-compatible package manager)
npm install
# or: yarn install / bun installnpm run build
# or: yarn build / bun run buildUnit tests:
npm test
# or: yarn test / bun run testEnd-to-end tests (requires API credentials):
cp .env.example .env.test
# fill in AIR_API_KEY and AIR_WORKSPACE_ID
npm run test:e2enpm run typecheck| Package | Description |
|---|---|
@air/api-core |
HTTP client, pagination, errors, retry logic |
@air/api-rest |
Resource classes and types |
@air/api-sdk |
Unified entry point (re-exports core + rest) |
This project uses Changesets to manage versioning and publishing.
When you make a change that should be published, add a changeset describing it:
npm run changeset:addThis launches an interactive prompt where you select the affected packages and bump type (patch, minor, or major), then write a short summary. It creates a markdown file in .changeset/ — commit this with your PR.
When your PR (with the changeset file) merges to main, the CI workflow automatically creates or updates a "chore: version packages" PR that bumps versions and updates changelogs.
Merge the "chore: version packages" PR. CI will publish the new versions to npm and create GitHub releases.
| Script | Description |
|---|---|
npm run changeset:add |
Add a new changeset (interactive) |
npm run version |
Apply changesets — bump versions and update changelogs (CI does this) |
npm run release |
Publish packages to npm (CI does this) |
- Node.js >= 18
- TypeScript >= 5.7 (if using TypeScript)