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-sdk

Quick 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",
  },
});
OptionTypeDefaultDescription
apiKeystringAIR_API_KEY env varYour API key
workspaceIdstringAIR_WORKSPACE_ID env varTarget workspace ID
baseURLstringhttps://api.air.inc/v1API base URL
maxRetriesnumber3Max automatic retries for retryable errors
timeoutnumber60000Request timeout in milliseconds
defaultHeadersobjectundefinedHeaders 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-id
const air = new AirApi(); // reads from env automatically

Custom 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):

  1. SDK defaults (x-api-key, x-air-workspace-id, user-agent: air-api-sdk/<version>)
  2. defaultHeaders from the constructor
  3. Per-request headers on 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:

StatusError ClassDescription
400BadRequestErrorInvalid request parameters
401AuthenticationErrorMissing or invalid API key
403PermissionErrorValid credentials, insufficient permissions
404NotFoundErrorResource doesn't exist
429RateLimitErrorRate limit exceeded
500+InternalServerErrorServer-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)