Authentication, rate limits and idempotency
Key validation order, rate-limit response headers, idempotency keys and request ID conventions.
Authentication
Every request is validated in this order: key exists and is enabled → not expired → IP allowlist → model exists and the group allows it → balance / quota / budget → rate limits and concurrency. Any failing step returns an error in the protocol's standard format with an error.code.
| Location | Form | Protocols |
|---|---|---|
| Authorization header | Bearer sk-… | OpenAI / Anthropic / video |
| x-api-key header | sk-… | Anthropic |
| x-goog-api-key header | sk-… | Gemini |
| apiKey in the request body | "apiKey": "ak-…" | Private protocol |
Rate limits and concurrency
Limits apply on three layers: key, user and group (RPM / TPM / concurrency). Exceeding them returns 429 with Retry-After.
| Field | Type | Description |
|---|---|---|
| X-RateLimit-Limit-Requests | header | Requests allowed in the current window. |
| X-RateLimit-Remaining-Requests | header | Requests remaining in the current window. |
| X-RateLimit-Reset-Requests | header | Seconds until the window resets. |
| Retry-After | header | Suggested seconds to wait before retrying when rate limited. |
Idempotency and request IDs
Creating requests (video tasks, private-protocol calls) are idempotent: standard protocols use the Idempotency-Key header, the private protocol uses the clientRequestId field. Replaying the same key from the same key owner within 24 hours returns the first result without charging again.
The X-Request-Id response header identifies the request; you may also send your own to align logs.
Look up a key's usage
GET/v1/key/usage
With the key itself, read its quota, limits, expiry, the last 30 days of usage by model and its 20 latest calls, without signing in and regardless of the key's usage API switch. Only this key's own data comes back: no account balance, no other keys, no platform cost. The web version is at /usage-lookup.
Each source IP may look up 10 times a minute and each key 30 times; beyond that the answer is 429 with Retry-After. An invalid, revoked or expired key answers 401, a disabled key 403. Amounts are micro-USD (1 USD = 1,000,000).
| Field | Type | Description |
|---|---|---|
| key | object | name, prefix, last4, status, expires_at (may be null), created_at. |
| quota | object | total / daily / monthly_micro_usd of null mean no cap; used_* is what was spent; remaining_* is set only against a cap and never below 0. |
| limits | object | rpm_limit, tpm_limit, concurrency_limit; null means no limit. |
| last_30_days | object | start, end, models[] (model, requests, errors, input_tokens, output_tokens, cached_tokens, charged_micro_usd) and a total with the same fields. |
| recent | array | The 20 latest calls: request_id, created_at, model, status_code, input_tokens, output_tokens, charged_micro_usd. |
curl https://www.link42.ai/v1/key/usage \
-H "Authorization: Bearer $LINK42_API_KEY"Key asset access
Every key has an asset access level (asset_access) that decides which assets it may reference as asset://<id> in a request. You can change it under Console → API keys when you create or edit a key.
Referencing an asset outside that scope returns exactly the same error as an asset that does not exist: not_found or reference_asset_unavailable for video tasks, reference_asset_unavailable for image generation. Nobody can use the error to find out whether an asset exists.
The same level decides which assets the key can read, download and delete through the /v1/assets endpoints; see "How asset access applies to the asset endpoints" on the Assets page.
| Value | Assets the key may reference |
|---|---|
| own | Default for new keys. Only the assets this key uploaded and the assets uploaded in the console. |
| all | Every asset in the account. |
| none | None; a request containing asset:// returns 403 asset_access_denied. |
- Keys created before this setting was introduced keep all, so existing integrations are unaffected; change them to own or none in the console to narrow them.
Compatibility profiles
A key is bound to an immutable compatibility profile. standard_v1 is the default for new keys; legacy_v1 preserves the old system's response media type, empty results and streaming behaviour and exists only for migrated keys. Changing profile means creating a new key; historical behaviour never changes silently.
| Behaviour | legacy_v1 | standard_v1 |
|---|---|---|
| Private protocol response type | text/plain | application/json |
| getResult miss | empty string | {"code":-1,"message":"task not found"} |
| Private protocol stream=true | buffered whole response | real SSE |
| Cross-user access | denied | denied |
Console API
Besides the gateway data plane (/v1, /v1beta, /server) the platform exposes a set of /api/v1 console endpoints for the account, keys, usage, ledger and agent wholesale ordering. They do not accept an API key: the credential is the browser session cookie, or a bearer token issued for the same session, and every query is scoped to the signed-in user.
A success answers {data, request_id}; paginated endpoints answer {list, total, page, page_size}. A failure answers the flat envelope {code, error_code, message, request_id}, described on the Error codes page.
- $LINK42_CONSOLE in the samples below is the console site origin, the domain you sign in to. It is not the gateway origin.
- Bodies are parsed strictly: an undeclared field answers 400 invalid_json. On update endpoints, omitting a field leaves it unchanged.
Update your profile
PATCH/api/v1/auth/me
Changes the signed-in user's nickname, interface language and region. All three fields are optional, so send only what changes; the response carries the updated user object.
| Field | Type | Description | |
|---|---|---|---|
| nickname | string | optional | Display name. |
| locale | string | optional | Interface language, for example zh-CN or en-US. |
| region | string | optional | Region code; it decides which payment methods and compliance policies apply. |
curl -X PATCH "$LINK42_CONSOLE/api/v1/auth/me" \
-H "Content-Type: application/json" \
-b "$LINK42_SESSION_COOKIE" \
-d '{"nickname": "Ada", "locale": "en-US"}'Two-step verification and confirming sensitive changes
GET/api/v1/auth/mfa/status
With two-step verification on, every sign-in (password, provider or single sign-on) also asks for a code emailed to the account address. A passkey sign-in is strong on its own and asks for no code.
Changing the password, closing the account, adding or removing a passkey, turning two-step verification on or off, linking or unlinking a sign-in provider and requesting a commission withdrawal are sensitive: they need the current password and an emailed code. An account without a password (created through a provider) needs only the code. A code is bound to one action and cannot confirm another.
| Endpoint | What it does | Body |
|---|---|---|
| GET /api/v1/auth/mfa/status | Reports whether two-step verification is on: {enabled}. | — |
| POST /api/v1/auth/sensitive-code | Emails the code for one sensitive action and answers 202 {expires_at, cooldown_seconds}. action is one of change_password, close_account, add_passkey, remove_passkey, two_factor, link_provider, unlink_provider, withdrawal. | {"action": "two_factor"} |
| POST /api/v1/auth/two-factor | Turns two-step verification on or off and answers {enabled}. | {"enabled": true, "password": "…", "code": "123456"} |
| POST /api/v1/auth/login/2fa | The full sign-in entry point that takes the code. | {"identifier": "…", "password": "…", "code": "123456"} |
| POST /api/v1/auth/passkeys/{id}/remove | Removes a passkey. Request the emailed code with action=remove_passkey first. | {"password": "…", "code": "123456"} |
- With two-step verification on, a sign-in without code answers 401 mfa_required and emails the sign-in code; submitting again without code sends it again, subject to the send cooldown. A wrong or expired code answers 401 mfa_invalid and counts toward the account lock.
- A wrong password or code on a sensitive action counts toward the account lock too; every code works once.
Password reset and change
POST/api/v1/auth/password/reset/request
A reset is two steps: ask for a code by e-mail, then set the new password with that code. Changing the password while signed in uses a separate endpoint and needs the current password plus a change_password code (request it from POST /api/v1/auth/sensitive-code); an account without a password can set its first one with the code alone.
| Endpoint | What it does | Body |
|---|---|---|
| POST /api/v1/auth/password/reset/request | Sends the reset code and answers 202 {expires_at, cooldown_seconds}. | {"email": "…", "captcha_token": "…"} |
| POST /api/v1/auth/password/reset/confirm | Sets the new password with that code; answers 204. | {"email": "…", "code": "…", "new_password": "…"} |
| POST /api/v1/auth/password/change | Changes the password while signed in; answers 204. | {"current_password": "…", "new_password": "…", "code": "123456"} |
- A successful change revokes every session except the current one, so other devices must sign in again.
- Asking too often answers 429 verification_cooldown with Retry-After and the X-RateLimit-* headers. Too many wrong codes answer verification_exhausted and a fresh code is needed. A weak new password answers weak_password; reusing the current one answers password_reused.
Linked sign-in providers
GET/api/v1/auth/identities
Lists, links and unlinks third-party sign-in identities. Which providers exist depends on the platform configuration, so query GET /api/v1/auth/oauth/providers first. Linking is confirmed with the current password and a link_provider code, unlinking with the current password and an unlink_provider code; request either code first with POST /api/v1/auth/sensitive-code and the matching action.
| Endpoint | What it does |
|---|---|
| GET /api/v1/auth/oauth/providers | Lists the enabled sign-in providers. |
| GET /api/v1/auth/oauth/{provider}/authorize | Signs in or signs up with a provider: starts the authorization redirect; the provider returns to the matching callback route. |
| POST /api/v1/auth/oauth/{provider}/link | Links a provider account to the signed-in user. Body {password, code}; answers {url, expires_at}, and the browser goes to url to authorize. |
| GET /api/v1/auth/identities | Lists the identities linked to the signed-in user. |
| POST /api/v1/auth/identities/{provider}/unlink | Unlinks a provider. Body {password, code}, where code is the emailed unlink_provider code. |
- One third-party account links to exactly one platform user: a second attempt answers 409 identity_already_linked, and linking a second account from the same provider answers 409 provider_already_linked.