OAuth

Air supports OAuth 2.0 Authorization Code with PKCE for integrations that need to call the Air Public API on behalf of an Air user.

Use this flow when your app needs user-authorized access across the workspaces that user can access in Air. Access tokens are accepted by https://api.air.inc/v1/... endpoints according to the scopes the user approved, the app's configured access, and the selected workspace for each request.

Choosing Between OAuth and API Keys

Air supports both OAuth and API-key authentication. Choose based on what your integration needs to represent: an individual Air user, or a trusted service connected to one Air workspace.

Use OAuth When

Use OAuth when your integration needs to act on behalf of an Air user.

OAuth is the recommended choice if:

This is usually the right choice for public third-party apps, marketplace integrations, and apps where each user's Air access matters.

Use an API Key When

Use an API key when your integration runs from a trusted backend or automation service for a specific Air workspace.

API keys may be the better choice if:

API keys are often simpler for server-to-server workflows, private CMS plugins backed by a server-side integration, and internal automation tools.

Important Security Guidance

Never expose an API key in browser, mobile, desktop, or iframe-based plugin code. Treat an API key like a password.

If your integration has a browser or CMS plugin frontend, use a backend service to store the API key and make Air API requests. The backend should authenticate and authorize the user in your app or CMS before calling Air.

Quick Rule

If Air needs to know which Air user is authorizing access, use OAuth.

If the integration is a private, trusted service for one workspace, and your backend can protect the key, an API key may be simpler.

Base URLs

Purpose Production URL
Air OAuth consent entrypoint https://app.air.inc/oauth/consent
OAuth token endpoint https://auth.air.inc/oauth2/token
Public API https://api.air.inc/v1

Start authorization at Air's consent URL. Direct authorization-server URLs, including https://auth.air.inc/oauth2/authorize, are not a supported integration entrypoint and will fail.

Client Provisioning

OAuth client provisioning is currently handled by Air. To request a client, contact Air Support at help@air.inc.

Include:

Air will provide the client_id. If your app is provisioned as a confidential client, Air will also provide a client secret. Public clients must not use a client secret.

Redirect URIs must match the provisioned values exactly.

Supported Grant Types

Supported:

Not supported:

Authorization Flow

  1. Generate a high-entropy state value.
  2. Generate a PKCE code_verifier.
  3. Derive code_challenge = BASE64URL(SHA256(code_verifier)).
  4. Redirect the user to https://app.air.inc/oauth/consent with the required OAuth parameters.
  5. The user signs in to Air if needed, reviews the Air consent screen, and chooses whether to allow access.
  6. If the user allows access, Air redirects the user back through the OAuth authorization flow.
  7. Your redirect URI receives code and state.
  8. Verify the returned state.
  9. Exchange code and code_verifier for tokens at https://auth.air.inc/oauth2/token.
  10. Use the access token as Authorization: Bearer ... when calling the Air Public API.

If the user denies access, Air redirects to your redirect_uri with:

error=access_denied&state=<state>

Required Authorization Parameters

Send users to https://app.air.inc/oauth/consent with these query parameters:

Parameter Required value
response_type code
client_id Your Air-provided OAuth client ID
redirect_uri One of the exact redirect URIs provisioned for your client
scope Space-separated Air scope names, such as assets.read workspace.read
state High-entropy value generated by your app and verified after redirect
code_challenge Base64url-encoded SHA-256 hash of your code_verifier
code_challenge_method S256

Use Air scope names without a prefix. For example, request assets.read, not public-api/assets.read.

Example authorization URL:

https://app.air.inc/oauth/consent?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fair%2Fcallback&scope=assets.read%20workspace.read&state=STATE_VALUE&code_challenge=CODE_CHALLENGE&code_challenge_method=S256

PKCE and State

PKCE is required. Use S256; plain-text PKCE is not accepted.

The code_verifier should be a cryptographically random string between 43 and 128 characters. Store it server-side or in a secure, same-user session until the callback arrives.

Example JavaScript PKCE helpers:

const base64UrlEncode = (bytes) =>
  btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, "-")
    .replace(/\/g, "_")
    .replace(/=+$/g, "");

const randomBytes = (length) => {
  const bytes = new Uint8Array(length);
  crypto.getRandomValues(bytes);
  return bytes;
};

export const createState = () => base64UrlEncode(randomBytes(24));

export const createPkcePair = async () => {
  const verifier = base64UrlEncode(randomBytes(32));
  const digest = await crypto.subtle.digest(
    "SHA-256",
    new TextEncoder().encode(verifier),
  );
  const challenge = base64UrlEncode(new Uint8Array(digest));

return { verifier, challenge };
};

Use state to protect against CSRF and to recover any app-local return path after the callback. Air returns the value unchanged on success and denial.

Token Exchange

Exchange the authorization code for tokens with application/x-www-form-urlencoded.

Public client example:

curl --request POST "https://auth.air.inc/oauth2/token" \
  --header "Accept: application/json" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "client_id=YOUR_CLIENT_ID" \
  --data-urlencode "redirect_uri=https://example.com/oauth/air/callback" \
  --data-urlencode "code=AUTHORIZATION_CODE" \
  --data-urlencode "code_verifier=ORIGINAL_CODE_VERIFIER"

Confidential client example, using HTTP Basic client authentication (preferred per RFC 6749 §2.3.1):

curl --request POST "https://auth.air.inc/oauth2/token" \
  --user "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  --header "Accept: application/json" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "redirect_uri=https://example.com/oauth/air/callback" \
  --data-urlencode "code=AUTHORIZATION_CODE" \
  --data-urlencode "code_verifier=ORIGINAL_CODE_VERIFIER"

Confidential clients may alternatively send client_id and client_secret in the form body; do not combine the two methods on the same request.

Successful token responses include an access token and expiration:

{
  "access_token": "eyJ...",
  "expires_in": 3600,
  "refresh_token": "eyJ...",
  "token_type": "Bearer",
  "scope": "public-api/assets.read public-api/workspace.read"
}

The refresh_token field is present only when the client is configured to receive refresh tokens. Store refresh tokens securely and never expose a confidential client secret in browser or native app code.

Refresh example:

curl --request POST "https://auth.air.inc/oauth2/token" \
  --header "Accept: application/json" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "client_id=YOUR_CLIENT_ID" \
  --data-urlencode "refresh_token=REFRESH_TOKEN"

For confidential clients, use HTTP Basic client authentication for refresh requests and omit client_id from the form body. Alternatively, include client_id and client_secret in the form body; do not combine the two methods on the same request.

Calling the Public API

Use the access token as a bearer token:

curl "https://api.air.inc/v1/assets?limit=25" \
  --header "Authorization: Bearer ACCESS_TOKEN" \
  --header "x-air-workspace-id: WORKSPACE_ID"

All workspace-scoped Public API routes require x-air-workspace-id. The value must be a UUID for a workspace the user can access.

Do not send x-api-key and Authorization: Bearer ... on the same request. A request with both auth methods is rejected.

Workspace Discovery

OAuth clients can discover workspaces with:

curl "https://api.air.inc/v1/workspaces" \
  --header "Authorization: Bearer ACCESS_TOKEN"

GET /v1/workspaces is bearer-only and requires workspace.read. Do not include x-air-workspace-id for this endpoint.

Response:

{
  "data": [
    {
      "id": "5bd304de-2ea4-435b-a065-69258f340e39",
      "name": "Launch"
    }
  ]
}

Use one of the returned workspace IDs as x-air-workspace-id on subsequent workspace-scoped API requests.

API Key and Bearer Token Rules

Air still supports API-key authentication for existing Public API integrations:

x-api-key: API_KEY
x-air-workspace-id: WORKSPACE_ID

OAuth bearer authentication uses:

Authorization: Bearer ACCESS_TOKEN
x-air-workspace-id: WORKSPACE_ID

Rules:

Consent and Workspace Permissions

The OAuth approval applies across the current Air workspaces where the app is available to the user. The app can only access data the approving user can already access in Air.

During consent, Air evaluates whether the user has the workspace permissions required by the requested scopes. If permissions are missing, Air shows the affected workspaces and permissions. The user cannot complete authorization until the missing permissions are resolved or the app's workspace availability changes.

For apps limited to specific workspaces, API requests are accepted only for workspaces where the app is available.

Connected-App Disconnects

Users can disconnect an OAuth app from Air account settings.

After revocation:

If an app is no longer available in Air, requests fail with the same grant_revoked reason. If the token's user has not authorized the app for their Air account, requests fail with extra.reason: "missing_grant".

Scopes

Request the least-privileged scopes your integration needs.

Scope Description
assets.read View assets, original files, versions, and discussions.
assets.write Create, edit, and delete assets.
boards.read View boards.
boards.write Create, edit, delete, and manage board members.
custom_fields.read View custom fields.
custom_fields.write Create, edit, delete, and apply custom field values to resources.
tags.read View tags.
tags.write Create, edit, delete, and apply tags to resources.
workspace.read View workspace information.
workspace_security.manage Manage workspace security settings and audit logs.

Current Public API Scope Requirements

The table below summarizes the OAuth scopes enforced by current Public API routes. Endpoint request and response schemas are documented in the Public API reference.

Endpoint area Required scopes
GET /v1/workspaces workspace.read; OAuth bearer only; no x-air-workspace-id
Read assets and asset versions assets.read
Create, update, delete, upload, import, or generate CDN links for assets assets.write
List boards containing an asset assets.read and boards.read
Add or remove tags on an asset version tags.write
Set custom field values on assets or boards custom_fields.write
Read boards boards.read
Create, update, delete boards or manage board guests boards.write
Add or remove assets on boards boards.write and assets.write
Read custom fields custom_fields.read
Create, update, or delete custom fields and custom field values custom_fields.write
Read tags tags.read
Create, update, or delete tags tags.write
Create imports and read import status assets.write
Get guest roles workspace.read
Read audit logs workspace_security.manage

Rate Limits

Default Public API limits:

Auth mode Limit
API key 15 requests per second and 10 concurrent requests per API key
OAuth bearer, per user and app 15 requests per second and 10 concurrent requests
OAuth bearer, per workspace and app 75 requests per second and 50 concurrent requests

GET /v1/workspaces is not workspace-scoped, so only the OAuth user-and-app limit applies.

Some endpoints may have additional route-specific limits. When a limit is exceeded, the API returns 429.

CORS

Air's Public API supports browser-based OAuth integrations.

Preflight requests:

OPTIONS /v1/assets HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: authorization,content-type,x-air-workspace-id

Successful preflight response:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,HEAD,PUT,POST,DELETE,PATCH
Access-Control-Allow-Headers: authorization,content-type,x-air-workspace-id
Access-Control-Max-Age: 7200
Vary: Origin

Actual Public API responses also include:

Access-Control-Allow-Origin: *
Vary: Origin

Use bearer tokens in the Authorization header. Do not rely on cookies for Public API authentication.

Error Contracts

Consent Errors

Invalid consent requests fail before the user can authorize. Common causes:

Consent API errors use Air API error objects. Example:

{
  "statusCode": 400,
  "type": "BAD_REQUEST",
  "message": "Invalid OAuth parameters.",
  "extra": {}
}

If the user lacks required workspace permissions when they try to allow the app:

{
  "statusCode": 403,
  "type": "FORBIDDEN",
  "message": "You cannot authorize this app for all current workspaces.",
  "extra": {
    "reason": "cannot_authorize_all_current_workspaces"
  }
}

Token Endpoint Errors

The token endpoint returns OAuth-standard errors for invalid code exchange, refresh, client authentication, or PKCE verification failures. Treat any non-2xx token response as a failed authorization and restart the Air consent flow when the error cannot be recovered.

Public API Auth Errors

Missing bearer token on OAuth-only routes:

{
  "error": "Unauthorized",
  "message": "Authentication required: provide a valid bearer token"
}

Missing auth on workspace-scoped routes when OAuth is available:

{
  "error": "Unauthorized",
  "message": "Authentication required: provide a valid x-api-key header or Authorization: Bearer token"
}

Missing workspace ID:

{
  "statusCode": 400,
  "type": "INVALID_INPUT",
  "message": "'x-air-workspace-id' is required",
  "error": "'x-air-workspace-id' is required"
}

Invalid workspace ID:

{
  "statusCode": 400,
  "type": "INVALID_INPUT",
  "message": "'x-air-workspace-id' is not a valid uuid",
  "error": "'x-air-workspace-id' is not a valid uuid"
}

Mixed API-key and bearer auth:

{
  "statusCode": 400,
  "type": "INVALID_INPUT",
  "message": "Provide either x-api-key or Authorization: Bearer token, not both",
  "error": "Provide either x-api-key or Authorization: Bearer token, not both"
}

OAuth authorization failures return 403 with a reason:

{
  "statusCode": 403,
  "type": "FORBIDDEN",
  "extra": {
    "reason": "missing_scope"
  }
}

Common OAuth extra.reason values:

Reason Meaning
invalid_token The token subject or client could not be resolved.
grant_revoked The user disconnected the app or the app is no longer available in Air.
missing_grant The token's user has not authorized this app for their Air account. Send the user through the Air OAuth consent flow.
missing_scope The token or app authorization does not include a required scope.
permission_denied The user does not have access to the requested workspace.
app_not_entitled_to_workspace The app is not available for the requested workspace.

Rate limits:

{
  "error": "Too Many Requests",
  "message": "Too Many Requests"
}

Temporary auth or rate limiter outages return 503 with error: "Service Unavailable".

Security Checklist