Air API SDK
The @air/api-sdk package is a typed TypeScript/JavaScript client for the Air API. It handles authentication, pagination, retries, error handling, and file uploads so you can focus on building your integration instead of managing low-level HTTP details.
Installation
Requires Node.js >= 18 (or Bun).
npm install @air/api-sdk
# or
yarn add @air/api-sdk
# or
bun add @air/api-sdkQuick Start
import { AirApi } from "@air/api-sdk";
const air = new AirApi({
apiKey: "your-api-key",
workspaceId: "your-workspace-id",
});
// List boards in your workspace
const page = await air.boards.list();
console.log(page.data);For details on obtaining your API key and workspace ID, see the Getting Started guide.
Configuration
Constructor options
const 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",
},
});| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | AIR_API_KEY env var | Your API key |
workspaceId | string | AIR_WORKSPACE_ID env var | Target workspace ID |
baseURL | string | https://api.air.inc/v1 | API base URL |
maxRetries | number | 3 | Max automatic retries for retryable errors |
timeout | number | 60000 | Request timeout in milliseconds |
defaultHeaders | object | undefined | Headers sent with every request |
Environment variables
Instead of passing apiKey and workspaceId 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 env automaticallyCustom headers
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:
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
Resources
Boards
Create, read, update, and delete boards. Manage sub-boards, board assets, custom field values, and guest access.
// 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: "[email protected]",
roleId: "role-id",
});
const guests = await air.boards.listGuests(board.id);
const filteredGuests = await air.boards.listGuests(board.id, { email: "[email protected]" });
await air.boards.updateGuest(board.id, guest.id, { roleId: "new-role-id" });
await air.boards.removeGuest(board.id, guest.id);Assets
List, search, and manage assets. Filter by board, tags, custom fields, search query, and date range.
// List assets
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");Asset Versions
Each asset can have multiple versions. Manage version metadata, download URLs, and tags.
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");Tags
Organize assets with tags. Tags can be applied to individual asset versions.
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);Custom Fields
Define custom metadata fields for your workspace. Supports single-select, multi-select, plain-text, and date types.
// 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);Roles
List available roles for guest access on boards.
const roles = await air.roles.list({ type: "guest" });
// Returns: [{ id, name, description, billable, type }]Uploads
Upload files directly from a file path or buffer. The SDK handles presigned URL generation, multipart uploads for large files, and progress tracking.
// Upload from file path
const result = await air.uploads.uploadFile(
{ filePath: "./photo.png" },
{ parentBoardId: "board-id" },
);
console.log(result.assetId, result.versionId);
// Upload 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. When uploading large files from a file path, the SDK streams chunks directly from disk instead of reading the entire file into memory.
For low-level control over the upload process using presigned URLs, use air.uploads.create() directly or see the Upload an Asset guide for a step-by-step walkthrough using the raw API.
Imports
Import assets from external URLs. Air will download and process the file on your behalf.
// 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'Pagination
List methods return a PagePromise that supports two patterns:
Auto-pagination
Use for await to iterate through all results across pages automatically:
for await (const asset of air.assets.list({ limit: 50 })) {
console.log(asset.id);
// automatically fetches subsequent pages
}Manual pagination
For more control, await the result and navigate pages explicitly:
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();
}Error Handling
All API errors extend APIError with status, body, and headers properties. Specific error classes are thrown based on HTTP status:
| Status | Error Class | Description |
|---|---|---|
| 400 | BadRequestError | Invalid request parameters |
| 401 | AuthenticationError | Missing or invalid API key |
| 403 | PermissionError | Valid credentials, insufficient permissions |
| 404 | NotFoundError | Resource doesn't exist |
| 429 | RateLimitError | Rate limit exceeded |
| 500+ | InternalServerError | Server-side error |
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);
}
}Automatic retries
Retryable errors are automatically retried with exponential backoff up to maxRetries times (default: 3). You don't need to implement your own retry logic for transient failures.
- GET and other idempotent methods: retries on 408, 429, 500, 502, 503, and 504
- POST requests: retries on 408 and 429 only (non-idempotent requests are not retried on server errors to avoid duplicate side effects)