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:
- Users should connect their own Air account.
- Air should enforce each user's workspace permissions.
- Your app is used across multiple Air workspaces or customers.
- Users should be able to disconnect the app from Air.
- You need user-level authorization instead of one shared integration identity.
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:
- The integration is private or internal.
- One workspace admin or service account should control access.
- Users do not need to log into Air individually.
- Your app only needs access to one Air workspace.
- Your backend can securely store the API key.
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 [email protected].
Include:
- App name and support contact.
- App logo URL, if available.
- Production redirect URI and any development or staging redirect URIs.
- Requested Air scopes.
- Whether your app is a public client, such as a browser or native app, or a confidential server-side client.
- Whether the app should be available to all Air workspaces or restricted to specific workspaces.
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:
- Authorization Code with PKCE:
grant_type=authorization_code - Refresh Token:
grant_type=refresh_token, when a refresh token is issued to the client
Not supported:
- Client Credentials
- Password
- Device Code
- Implicit
Authorization Flow
- Generate a high-entropy
statevalue. - Generate a PKCE
code_verifier. - Derive
code_challenge = BASE64URL(SHA256(code_verifier)). - Redirect the user to
https://app.air.inc/oauth/consentwith the required OAuth parameters. - The user signs in to Air if needed, reviews the Air consent screen, and chooses whether to allow access.
- If the user allows access, Air redirects the user back through the OAuth authorization flow.
- Your redirect URI receives
codeandstate. - Verify the returned
state. - Exchange
codeandcode_verifierfor tokens athttps://auth.air.inc/oauth2/token. - 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=S256PKCE 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_IDOAuth bearer authentication uses:
Authorization: Bearer ACCESS_TOKEN
x-air-workspace-id: WORKSPACE_IDRules:
GET /v1/workspacesaccepts only OAuth bearer tokens.- All other
/v1/...Public API routes accept OAuth bearer tokens and, where API-key access is enabled, API keys. - Send exactly one auth method per request.
- OAuth requests are authorized by the approved token scopes, the app's configured access, the selected workspace, and the user's live Air workspace permissions.
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:
- Existing access tokens stop working for API access.
- Requests fail with
403andextra.reason: "grant_revoked". - To reconnect, send the user through the Air OAuth consent flow again.
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-idSuccessful 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: OriginActual Public API responses also include:
Access-Control-Allow-Origin: *
Vary: OriginUse 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:
- Missing required OAuth parameter.
response_typeis notcode.code_challenge_methodis notS256.- Blank
client_id,state, orcode_challenge. - Unknown, blocked, or unavailable client.
redirect_uriis not registered for the client.- Requested scope is unknown or not allowed for the client.
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
- Start every authorization at
https://app.air.inc/oauth/consent. - Use Authorization Code with PKCE and
code_challenge_method=S256. - Generate and verify a high-entropy
statevalue. - Store refresh tokens securely.
- Never put client secrets in browser, mobile, desktop, or other public-client code.
- Do not log authorization codes, access tokens, refresh tokens, or full callback URLs.
- Request only the scopes your integration needs.
- Use
GET /v1/workspacesto let the user choose the workspace before making workspace-scoped requests.