Skip to main content
Use this guide to implement experiences such as Protect with . The partner registers one App, then each customer connects it once to the organization they choose. Every isolated customer installation can protect any number of external keys without receiving a different Client ID or secret. The contract is provider-neutral and applies to every provider registered and enabled in .
This is a confidential-backend integration. Never request an PAT or Service Account token, and never send the OAuth client secret, provider key, access token, refresh token, or runtime gateway token to browser storage, URLs, analytics, or logs.

What the integration creates

The first successful protection creates one internal Service Account and one dedicated project for the OAuth installation. The user who approved the connection is added to that project as ProjectAdmin. Every protected external key then receives its own:
  • encrypted provider credential;
  • independently configured gateway and model bindings; and
  • runtime gateway token limited to the data-plane run scope.
Disconnecting revokes OAuth/runtime access and disables the internal Service Account. It does not delete projects, credentials, gateways, settings, request logs, or audit history.

Before you start

An organization member with oauth-clients:create creates an OAuth App under Settings → Integrations → My OAuth Apps. Organization owners receive that permission by default. Enable Allow other organizations to install this app for a public App. The App definition, Client ID, secret, redirects, and publisher controls remain in the creator’s organization; every installation and resource created with its tokens belongs to the customer organization selected at consent. Leave it off for a private App that can connect only to its creator organization. The creator provides the partner developer with:
  • the Client ID;
  • the one-time Client secret; and
  • the exact HTTPS redirect URI registered for the partner backend.
Register these scopes:
  • protections:provision — Resolve providers, validate a provider key, discover models, run preflight, and provision resources atomically.
  • protections:read — Read only protections created by this OAuth connection.
  • protections:update — Update only protections created by this OAuth connection.
  • protections:manage — Revoke, reactivate, or rotate credentials only for protections created by this OAuth connection.
  • protections:read-logs — Read request metadata only for protection runtime tokens created by this OAuth installation. Request and response bodies are never exposed through OAuth.
  • protections:read-spend — Read token usage and monetary spend only for protections created by this OAuth installation.
  • offline_access — Renew access automatically with rotating refresh tokens. The integration must be authorized again after 90 days without a successful refresh.
Enable all seven scopes for the complete protection lifecycle and observability. A deliberately limited integration may enable a subset, but its authorization request must match that subset exactly. OAuth rejects the complete authorization request when it contains even one permission the App does not allow. protections:provision is the required minimum and the UI keeps it enabled; offline_access cannot replace it. Permission changes are least-privilege and fail closed. Removing a permission narrows every existing installation immediately and revokes current access tokens. Removing offline_access also revokes refresh-token families. Adding a permission never upgrades an existing installation silently: the user must authorize the connection again. Redirect URIs use exact HTTPS matching. Wildcards, fragments, and runtime-selected return URLs are not accepted. The redirect URI is owned by the partner backend, not by . Production integrations normally use a route such as https://partner.example.com/oauth/callback. A local reference app may register an HTTPS tunnel ending in /callback, provided the authorization request uses that exact same value.

OAuth endpoints

Open Discover the authorization server in the API Reference. Its Mintlify base-URL selector switches the request URL and generated examples between Production and QA from the OpenAPI servers contract. Local development values are listed in Environments. Never infer one environment from credentials issued by another. uses OAuth 2.0 Authorization Code with PKCE S256 for confidential web clients. It is not an OpenID Connect login provider and intentionally does not expose ID Tokens or UserInfo. Use OAuth protocol for complete discovery, authorization, callback, token, refresh, revocation, success, and error payloads for production, QA, and local environments.

Choose an implementation path

The SDK is optional. Both paths use the same OAuth scopes, endpoints, payloads, idempotency contract, and installation boundaries.

Option A: Node or TypeScript SDK

Use @airctrl/sdk when the confidential partner backend runs Node or TypeScript. It provides typed helpers for PKCE, token exchange and rotation, provider/model discovery, preflight, provisioning, resource reads, updates, and revocation.

Option B: Direct HTTP and OpenAPI

Use any standards-compliant OAuth and HTTP client in another language. No package is required. Read the machine contract from https://docs.airctrl.dev/api-reference/openapi.json and the authorization-server endpoints from the discovery URL above. The examples below use the SDK for brevity. Direct HTTP implementation maps the same flow to raw requests. For every request and response payload, lifecycle endpoint, and multi-key mapping, see OAuth protection API.

Connect once

If you chose the SDK path, install @airctrl/sdk in the confidential partner backend and construct one client:

1. Start authorization

Generate state and PKCE on the backend. Store state and the verifier in the user’s short-lived server session before redirecting the browser.
For a public App, lists only organizations where the approving person is an active member, holds oauth-installations:connect, and has active product access. If more than one qualifies, the consent screen requires an explicit organization selection. A private App shows only its creator organization. Active subscription, trial, and payment-retry (past_due) states qualify. The selected organization becomes the permanent tenant boundary for that installation. Its plan, quotas, billing, project, Service Account, credentials, gateways, tokens, logs, and spend are used; the publisher organization does not receive access to unrelated customer resources. The confidential partner backend uses the same authorization, token, protection, logs, and spend operations for private and public Apps. Distribution changes only which organization may be selected during consent and therefore which organization becomes the installation boundary.

2. Handle the callback

Require the same state, exact issuer, a non-expired server session, and an authorization code. Then exchange the code from the backend:
The connection is now reusable. Do not repeat OAuth for each protected key.

Protect one external key

The external platform already knows the selected key’s stable ID, display name, and secret. These values come from its backend—not from UI fields.

1. Resolve the provider

Do not hard-code provider UUIDs. Resolve the registered provider by slug:

2. Run preflight before sending the provider key

Preflight performs no writes and receives no provider key. Use it before provisioning so subscription, entitlement, and quota failures appear before the partner sends a provider key.

3. Validate the key and load live models

Retrieve the selected key from the partner’s server-side secret store and send it directly to :
uses the OAuth-only /v1/oauth/protections/providers/{providerId}/models operation. It uses the key only for upstream validation and live model discovery at this stage; it does not persist it. The confidential partner backend chooses the gateway name and allowed model IDs that it sends in the provisioning command. A partner may offer an optional review screen, but does not require another user step. Names, models, limits, logging, guardrails, and entitled OTel settings can be changed later through the installation-bound update operation.

4. Provision atomically

externalResourceId must be the stable, non-secret key ID from the partner platform. Generate one idempotency key per protection attempt and persist it until the final outcome is known.
Provisioning is all-or-nothing. validates the key and selected models again before writing, encrypts the provider key at rest, and never returns it. A successful response includes the runtime gateway token plaintext once:
For a one-click partner experience initiated from Protect with , keep that one stable external key ID in the partner’s server-side OAuth state. After consent, run provider resolution, preflight, model discovery and this provisioning command only for that pending key. Store the resulting protectionId beside it; the user does not repeat these inputs in . A connection-only flow must stop after token exchange and list existing protections—it must not call POST /v1/oauth/protections without an explicit pending protection intent. Retry an uncertain result with the same idempotency key and identical body. Reusing that key with a different body returns 409.

Use and manage the protected gateway

Use the returned gateway slug and runtime token from the partner backend. Do not append the provider’s native_base_path to a gateway URL: that field describes the provider upstream and is not an data-plane route. Use the SDK’s shared connection builder so the environment and encoded gateway slug follow the same contract as the Web app and MCP:
Use compatBaseUrl for the compatibility surface. For a provider-native operation, select the typed base returned by buildGatewayConnectionConfig() for that provider. See Gateway connection URLs and Connect an AI client. The OAuth installation may later list its own protected resources or update the gateway name, model bindings, and allowlisted settings:
Changing gatewayName also changes the derived gateway slug. Use updatedProtection.gateway.slug to rebuild connection URLs before sending more traffic. It cannot read or modify gateways created outside its own OAuth installation. With the matching scopes, read request metadata and spend for one protection without OTel:
These endpoints include only traffic attributed to runtime tokens owned by this OAuth installation’s internal Service Account, including rotated replacements. Customer-created tokens on the same gateway are excluded. Log cost is null unless the token also has protections:read-spend. The behavior is identical for a private same-organization installation and a public installation bound to the customer organization selected during consent.

Choose internal logs or OpenTelemetry

builds one canonical request-log event and exposes the same fields through its internal logs and OpenTelemetry export; only the transport envelope differs. This prevents internal storage and exported telemetry from disagreeing about identity, provider, model, operation, status, errors, tokens, cost, bytes, latency, streaming, guardrails, trace ID, or span ID. OAuth access tokens can poll protection-scoped request metadata with protections:read-logs and totals with protections:read-spend. They cannot read stored request or response bodies. Authorized users retain the broader human observability surfaces. OTel remains optional for real-time delivery to customer-controlled infrastructure. It is not required to read the OAuth log or spend endpoints. OTel is subject to the organization’s effective feature entitlement. Call preflight with otel=true before offering it. otel_not_available blocks only the requested OTel configuration, not a protection that omits OTel. Request/response bodies can be exported only when gateway logging and body capture are both enabled; disabling either automatically disables OTel body export.

Direct HTTP implementation

The REST path follows the same order as the SDK path:
  1. Read /.well-known/oauth-authorization-server and redirect the browser to its authorization_endpoint with response_type=code, exact redirect_uri, registered scopes, high-entropy state, and PKCE S256.
  2. Exchange the callback code at POST /v1/oauth/token using HTTP Basic client authentication and a form-encoded body.
  3. Call GET /v1/oauth/protections/providers with the OAuth bearer and resolve the provider by slug.
  4. Call GET /v1/oauth/protections/preflight?providerId={providerId} before sending the key.
  5. Call POST /v1/oauth/protections/providers/{providerId}/models with { "apiKey": ... } from the confidential backend.
  6. Call POST /v1/oauth/protections with the OAuth bearer, JSON body, and a stable Idempotency-Key header.
  7. Store the returned protectionId next to the external key ID. Use GET and PATCH /v1/oauth/protections/{protectionId} only for that protection.
  8. Use the protection lifecycle endpoints to revoke, reactivate, or rotate only that external key. Runtime-token rotation and reactivation require a stable Idempotency-Key.
  9. Poll GET /v1/oauth/protections/{protectionId}/logs and GET /v1/oauth/protections/{protectionId}/spend only when the App and installation grant the corresponding scopes.
  10. Rotate at POST /v1/oauth/token with grant_type=refresh_token; revoke through POST /v1/oauth/revoke.
Exchange the authorization code from Bash:
Resolve providers and run preflight without transmitting the provider key:
For model discovery and provisioning, build JSON inside the backend from its secret store. The payload shapes are:
Lifecycle requests use the returned protectionId:
Runtime-token rotation and reactivation require Idempotency-Key. Request/response examples for every operation are in OAuth protection API. Do not put the provider key or OAuth bearer in a URL, browser form, shell history, or committed file.

Refresh and disconnect

Access tokens expire after 15 minutes. With offline_access, use the latest stored refresh token and replace it after every successful refresh. Each successful refresh renews the 90-day inactivity window; replaying an older token revokes the token family. Serialize refreshes per installation. If two workers use the same refresh token concurrently, treats the second use as replay and revokes the complete token family. Use a distributed lock or an atomic compare-and-swap in the encrypted token store. When refresh returns invalid_grant, stop retrying and start authorization again.
When the partner stops using its OAuth token, revoke the current access or refresh token and erase the partner’s encrypted token copy:
RFC 7009 token revocation ends that OAuth token or refresh family; it does not delete resources. When an user disconnects the installation from Settings → Integrations → Connected apps, additionally disables runtime gateway tokens and the installation’s internal Service Account without deleting resources or audit history. Reauthorizing the same OAuth App for the same organization reuses that installation, its dedicated project, Service Account, provider credentials, gateways, and stable protection IDs. The partner should list the existing protections and call reactivate only for those it wants to restore. Each reactivation rechecks current entitlements and returns a new runtime gateway token once; it does not create replacement resources. Connect and disconnect events remain available in the connection history. Each connection-history event stores the exact scope snapshot granted at that moment. Reauthorization replaces the installation’s current scopes with the newly approved set; the historical snapshots remain unchanged for audit comparison. Protection list/read responses expose state as active, connection_suspended, or revoked. After reauthorization, automate restoration only for connection_suspended protections selected by the partner’s current intent. Never automatically restore revoked protections. The same OAuth access token may reactivate several existing protections and provision additional external keys; each command is independently authorized and idempotent.

Errors a partner must handle

  • oauth_redirect_uri_invalid — The callback does not exactly match registration. Use the registered HTTPS URI verbatim.
  • oauth_state_invalid / oauth_state_mismatch — State is missing, malformed, or different from the stored transaction. Abort and start a new authorization.
  • oauth_authorization_request_expired — The pending consent request expired or was already decided. Start authorization again; do not reuse it.
  • oauth_scope_insufficient — The token lacks the required delegated scope. Reconnect requesting only registered required scopes.
  • oauth_access_not_allowed — The endpoint is not part of the OAuth surface. Do not substitute a PAT; use documented OAuth operations.
  • subscription_inactive — The owning organization no longer has active product access. Active subscription, trial, and payment-retry (past_due) states qualify; other states must return to Billing before integration continues.
  • provider_key_invalid — The provider rejected the supplied key. Ask the user to repair the key in the external platform.
  • provider_model_not_available / provider_model_not_supported — A requested model is absent from the live catalog or cannot safely route it. Refresh model discovery and let the user choose only supported IDs.
  • provider_endpoint_not_configured / provider_validation_unavailable — Provider discovery or validation is temporarily unavailable. Do not persist the key; retry with bounded backoff.
  • Preflight blockers — Subscription, entitlement, or quota prevents protection. Show blockers before sending the provider key.
  • idempotency_key_invalid — The key is shorter than 16 characters or longer than 200. Generate a new stable operation key before sending the write.
  • idempotency_key_reused / idempotency_operation_in_progress — The key was reused with a different body or the original transaction has not committed. Never change the body; reconcile or retry the identical request.
  • idempotency_operation_superseded — A newer runtime-token operation already replaced the result of the retried lifecycle command. Keep the newest successful token and do not repeat the old operation with a new key.
  • oauth_external_resource_already_protected — This connection already mapped the same stable external resource ID. Read the existing protections instead of creating a duplicate.
  • oauth_gateway_protection_revoked / oauth_gateway_protection_connection_suspended — The protection cannot be updated or rotated in its current state. Reauthorize the connection when suspended, then call reactivate deliberately.
  • oauth_project_unavailable / oauth_service_account_inactive / oauth_service_account_unrecoverable — The preserved internal project or Service Account cannot currently serve the connection. Stop automation and require an authorized user to review the installation.
  • otel_endpoint_required — The protection has no OTel configuration yet. Include endpoint the first time; later PATCH requests may omit it.
  • otel_headers_invalid — The request exceeded 32 headers, used an invalid/forbidden name, or a value exceeded 2,048 characters. Correct the server-side OTel configuration.
  • otel_not_available — Effective account entitlements do not enable OTel. Continue without OTel or let an authorized user review internal logs; do not mention internal entitlement grants.
  • log_bodies_require_logging / otel_bodies_require_gateway_log_bodies — Enable gateway logging and body capture before requesting body export.
  • 429, 500, 502, 503 — Temporary capacity or upstream failure. Retry with bounded backoff and the same idempotency key when provisioning.

Security checklist

  • Keep all secrets and OAuth tokens in the confidential backend.
  • Use exact HTTPS redirect URIs, PKCE S256, high-entropy state, and exact issuer validation.
  • Resolve provider IDs from the registry instead of hard-coding them.
  • Run preflight before transmitting a provider key.
  • Treat externalResourceId and idempotency keys as stable per protection.
  • Never log request authorization headers, provider keys, runtime gateway tokens, or OAuth token bodies.
  • Revoke tokens on disconnect; do not delete resources to simulate revocation.