Authentication
The AI92 API supports two authentication schemes, both presented as a bearer token in the Authorization header:
- API keys — for your own server-to-server integrations. Simplest; start here.
- OAuth 2.0 — for apps that act on behalf of other AI92 users (third-party integrations, marketplace apps).
The gateway routes by token shape automatically: a token starting with ai92_live_ or ai92_test_ is treated as an API key; a token in header.payload.signature (JWT) form is treated as an OAuth access token.
Authorization: Bearer ai92_live_aBcD1234XXXXXXXXXXXXXXXXXXXXXXXXA missing or malformed header returns 401 AUTHENTICATION_REQUIRED. An unrecognized token returns 401 INVALID_API_KEY.
API keys
Prefixes
| Prefix | Environment | Spends credits? |
|---|---|---|
ai92_live_ | Production | Yes |
ai92_test_ | Sandbox | No — never |
The prefix is the switch. The same code, hitting the same host, runs against sandbox or production purely based on which key you send. See Sandbox.
How keys are stored
Keys are generated as <prefix><random> and the plaintext is returned exactly once, at creation. AI92 stores only a SHA-256 hash of the key plus a non-secret display prefix (e.g. ai92_live_aBcD1234) and the last four characters. We cannot recover a lost key — if you lose it, rotate.
Creating a key
Once you have one key, you can mint more programmatically:
curl -X POST https://api.ai92.ai/v1/account/keys \
-H "Authorization: Bearer ai92_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production-worker",
"kind": "production",
"expires_in_days": 365,
"daily_credit_limit": 5000,
"monthly_credit_limit": 100000,
"ip_whitelist": ["203.0.113.0/24"]
}'The response includes plaintext_key once:
{
"id": "9b1c...",
"name": "production-worker",
"kind": "production",
"prefix": "ai92_live_aBcD1234",
"last4": "9f2k",
"plaintext_key": "ai92_live_aBcD1234...9f2k",
"expires_at": "2027-06-21T00:00:00Z",
"daily_credit_limit": 5000,
"monthly_credit_limit": 100000,
"warning": "This is the only time the full key will be shown. Store it securely."
}Per-key controls
Each key can carry independent guardrails — useful for isolating a worker, a customer, or an environment:
expires_in_days— auto-expiry. After expiry, the key returns401 EXPIRED_API_KEY.ip_whitelist— a list of CIDRs. Requests from outside them return403 IP_NOT_ALLOWED. The first hop ofX-Forwarded-Foris trusted (behind the AI92 edge).daily_credit_limit/monthly_credit_limit— per-key spending caps (0= unlimited). Exceeding them returns402 KEY_SPENDING_LIMITwhile the rest of your account keeps working.
Edit these later with PATCH /v1/account/keys/{key_id}.
Rotation
Keys never auto-rotate. To rotate: create a new key, deploy it, then revoke the old one with DELETE /v1/account/keys/{key_id}. Revocation is immediate — a revoked key returns 401 INVALID_API_KEY on its very next use. Rotate on a schedule (90 days is a good default), on staff changes, and immediately on any suspected leak.
Security rules
- Never embed a key in client-side code (browser JS, mobile bundles, public repos). Keys are full-account bearer credentials. Anything reachable by a browser will leak. Call AI92 from your server and proxy to your front end.
- Prefer scoped, IP-restricted, credit-capped keys for each workload over one omnipotent key.
- Store keys in a secret manager, not in
.envcommitted to git. - If a key leaks, revoke first, investigate second.
OAuth 2.0
Use OAuth when your app needs to call AI92 on behalf of an AI92 user who is not you — for example a third-party tool a customer connects to their AI92 account. The user grants your app specific scopes; you receive an access token (a signed JWT) that grants only those scopes.
API keys grant all scopes for your account. OAuth tokens grant a subset of scopes for another account.
Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/v1/oauth/authorize | GET | Authorization Code consent screen |
/v1/oauth/token | POST | Exchange code → tokens; refresh tokens |
/v1/oauth/introspect | POST | Check whether a token is active |
/v1/oauth/revoke | POST | Revoke an access or refresh token |
/v1/.well-known/jwks.json | GET | Public signing keys (verify tokens) |
/v1/.well-known/openid-configuration | GET | OIDC discovery document |
/v1/oauth/userinfo | GET | User profile, scoped to the grant |
Authorization Code flow with PKCE
PKCE (S256) is required for public clients and recommended for all. The flow:
1. Generate a PKCE verifier + challenge, then redirect the user to consent:
https://api.ai92.ai/v1/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=contacts:read%20contacts:write%20offline_access
&code_challenge=BASE64URL_SHA256_OF_VERIFIER
&code_challenge_method=S256
&state=RANDOM_CSRF_TOKENSupported response_type is code. Requesting an unknown scope returns invalid_scope; omitting a required code_challenge returns code_challenge_required.
2. Exchange the returned code for tokens:
curl -X POST https://api.ai92.ai/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code=THE_CODE_FROM_CALLBACK \
-d redirect_uri=https://yourapp.com/callback \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d code_verifier=YOUR_PKCE_VERIFIER{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJ...",
"scope": "contacts:read contacts:write offline_access"
}Access tokens live 1 hour. Refresh tokens live 90 days and are only issued when offline_access is in the granted scope.
3. Refresh when the access token expires:
curl -X POST https://api.ai92.ai/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=refresh_token \
-d refresh_token=YOUR_REFRESH_TOKEN \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET4. Call the API with the access token exactly like an API key:
curl "https://api.ai92.ai/v1/contacts/search?q=saas+founder&country=US" \
-H "Authorization: Bearer eyJ..."Revocation is real-time
POST /v1/oauth/revoke invalidates a token immediately — the gateway checks each access token against the revocation table on every request, so a revoked or stolen JWT stops working at once rather than lingering until its TTL.
Token verification
Verify tokens offline against the JWKS at /v1/.well-known/jwks.json (RS256). Keys rotate roughly every 30 days; the previous key stays published in the JWKS for a 30-day grace window, so cache the JWKS and refresh it on an unknown kid.
Scopes
| Scope | Grants |
|---|---|
contacts:read | Read contacts |
contacts:write | Reveal & enrich contacts |
campaigns:read | Read campaigns |
campaigns:create | Create campaign objects |
campaigns:send | Send campaign messages |
seo:read | Read SEO data |
markets:read | Read market analytics |
reviews:read | Read Google Reviews |
reviews:reply | Reply to Google Reviews |
cloud:read | Read cloud storage |
cloud:write | Write cloud storage |
webhooks:manage | Manage webhooks |
account:read | Read account info |
account:write | Modify account settings (e.g. auto-recharge) |
offline_access | Issue refresh tokens |
A token missing a scope required by an endpoint returns 403 FORBIDDEN_SCOPE.
Which should I use?
| If you are… | Use |
|---|---|
| Building your own backend integration | API key (ai92_live_) |
| Testing | Sandbox API key (ai92_test_) |
| Building an app that other AI92 users connect | OAuth 2.0 with PKCE |
| Acting only within your own account | API key — it grants all scopes, no flow needed |