Connect flow
Step by step — authorize with PKCE, exchange the code, call /connect/key, store the gateway key. With a browser CORS note.
The connect flow is ordinary OpenID Connect plus one codai endpoint. When it ends you hold a codai_… gateway key that belongs to your user, scoped to their consent to your app, and you call https://ai.codai.ro exactly as the gateway docs describe.
your app ──1 redirect──▶ auth.codai.ro/auth ──login + consent──▶ your redirect_uri?code=…&state=…
your app ──2 POST /token (code + code_verifier)──▶ { access_token, id_token, … }
your app ──3 GET /connect/key (Bearer access_token)──▶ { api_key: "codai_…", base_url, … }
your app ──4 Authorization: Bearer codai_… ──▶ ai.codai.ro/v1/… (as the user)Build the authorize URL
PKCE is mandatory: generate a random code_verifier (43 – 128 URL-safe chars), derive code_challenge = base64url(sha256(verifier)), and keep the verifier plus a random state in the user's session.
GET https://auth.codai.ro/auth
?client_id=<your client_id>
&response_type=code
&redirect_uri=<a registered redirect URI, exact match>
&scope=openid%20email%20profile%20inference%20keys:manage
&state=<random>
&code_challenge=<S256 of the verifier>
&code_challenge_method=S256Optional additions: offline_access in scope for a refresh token (only if your client has the refresh_token grant), nonce if you validate ID tokens the strict way, acr_values=urn:codai:acr:mfa to require a second factor, resource=https://ai.codai.ro/v1 to be explicit about the audience (it is also the default).
The user signs in (or signs up) and sees a consent screen listing your requested scopes. Cancel sends them back with ?error=access_denied&error_description=User%20cancelled%20the%20authorization.
Exchange the code
Back at your redirect_uri, check state, then POST /token:
curl https://auth.codai.ro/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=https://app.example.com/callback" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "code_verifier=$VERIFIER"A confidential client authenticates here too — Authorization: Basic base64(client_id:client_secret) (client_secret_basic), client_secret in the form (client_secret_post), or a client_assertion JWT (private_key_jwt). A public client sends client_id only.
Response:
{
"access_token": "opaque…",
"id_token": "eyJhbGciOiJSUzI1NiIs…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid email profile inference keys:manage",
"refresh_token": "…only with offline_access…"
}The access token is opaque and lives one hour. You need it for exactly one more call.
Mint the gateway key
GET /connect/key with the access token. The token must carry keys:manage or inference.
curl https://auth.codai.ro/connect/key \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"api_key": "codai_xxxxxxxx",
"api_key_id": "3d6b…-uuid",
"base_url": "https://ai.codai.ro/v1",
"model": "codai",
"already_issued": false
}Prop
Type
One key per consent. The IdP never stores the plaintext. If you call /connect/key again for the same grant you get { "api_key": null, "already_issued": true, … } — there is nothing to retrieve. Persist the key (encrypted) the moment you receive it. Lost it? Send the user through the authorize step again: a fresh consent is a fresh grant and mints a fresh key, and the user can revoke the old one in the hub.
Consent grants live 90 days; when one expires the user reconnects and you store the new key.
| Status | error | Meaning |
|---|---|---|
401 | invalid_token | No bearer, or Token unknown or expired. — one hour has passed, or the token was revoked. |
403 | insufficient_scope | The token lacks both keys:manage and inference, or the grant carries no key-minting scope. |
400 | invalid_grant | The token has no consent grant behind it (a client-credentials style token cannot mint keys). |
Call the gateway as the user
import OpenAI from 'openai';
const client = new OpenAI({ apiKey: connected.api_key, baseURL: connected.base_url }); // base_url already ends in /v1
const reply = await client.chat.completions.create({ model: connected.model, messages: [{ role: 'user', content: 'Hello from my app' }] });Usage lands on the user's plan and shows up in their hub under the key labelled with your app's name. Send X-Codai-Client: <your-app>/<version> so their task list attributes it correctly.
Calling /connect/key from a browser
/connect/key answers CORS preflights only for an allow-list of origins, and the allow-list is codai's, not derived from your redirect URIs. Concretely:
- No
Originheader (server-to-server, curl, native apps) → the request works and no CORS headers are added. - Allowed origin →
Access-Control-Allow-Origin: <origin>,Vary: Origin; preflight204withAccess-Control-Allow-Methods: GET, OPTIONS,Access-Control-Allow-Headers: authorization, content-type, x-codai-device-name,Access-Control-Max-Age: 86400. - Any other origin → preflight
403 { "error": "origin_not_allowed" }; a directGETstill runs but the browser cannot read the response.
A third-party single-page app cannot call /connect/key cross-origin. Do the code exchange and the key mint on your backend, then hand the key (or, better, a session bound to it) to the browser. /token is different — its CORS follows your registered redirect URIs for public clients, so a SPA may exchange the code directly and still needs the backend only for /connect/key.
If your product genuinely needs a browser-only flow, ask us to add your origin to the allow-list when the client is registered.
Refreshing and revoking
- With
offline_access,POST /tokenwithgrant_type=refresh_token&refresh_token=…&client_id=…returns a new access token and — for public clients always, for confidential ones past 70 % of TTL — a rotated refresh token. The gateway key does not expire with the access token; you only need a fresh access token if you want to call/meor re-check the grant. POST /token/revocationwithtoken=<refresh_token>ends the session on your side. Revoking the grant (the user disconnecting in the hub) revokes the gateway key too; your next call toai.codai.rois401 invalid_api_key— prompt the user to connect again.GET /mewith the access token returns the claims for the granted scopes:sub,email,email_verified,name,picture, plusroleandsid.
Sign in with codai
auth.codai.ro is a standard OpenID Connect provider. Use it when your app needs to act on behalf of a codai user; use a plain API key when it does not.
Scopes and claims
Every scope auth.codai.ro issues, what the user sees on the consent screen, which claims and resources each one unlocks.