Account & Security
Authentication
Authenticate API requests with long-lived account API keys or short-lived, scope-restricted Scoped JWTs.
DEVUP AI supports two Bearer credentials that work with the official OpenAI and Anthropic SDKs as well as direct HTTP calls:
| Credential | Prefix | Model Scope | Lifetime & Spend Scope | Typical Use |
|---|---|---|---|---|
| Account API Key | sk-devup-... | All catalog models | No automatic expiry; full account DZD balance | Your own servers, CLI tools, and account endpoints |
| Scoped JWT | eyJ... | 1–50 selected models or * | 1 hour to 90 days; 1–100,000 DZD token limit | Delegated services, isolated workloads, and short-lived sessions |
Account API keys
Create and manage account API keys in Dashboard → API Keys. Send your key in the Authorization header on every request:
Authorization: Bearer $DEVUP_API_KEYOn GET /v1/models, GET /v1/models/{model}, GET /v1/me, GET /v1/me/avatar, and POST /anthropic/v1/messages, the x-api-key header is also accepted. When both Authorization and x-api-key are sent, Authorization takes precedence on /v1/* endpoints.
Scoped JWT overview
A Scoped JWT is a server-signed JSON Web Token (eyJ...) that grants bounded inference access without exposing your primary sk-devup-... API key. Each Scoped JWT binds three independent controls directly to the credential:
- Permitted models — restrict calls to an explicit allowlist of 1–50 catalog models, or allow all models (
*). - Fixed expiration — automatically expires after a chosen duration between 1 hour and 90 days (2,160 hours), and can be revoked immediately at any time.
- Per-token DZD spending limit — tracks cumulative DZD usage attributed to the token between 1 DZD and 100,000 DZD against your account balance.
Scoped JWTs authenticate inference requests across POST /v1/chat/completions, POST /v1/responses, POST /anthropic/v1/messages, POST /v1/embeddings, POST /v1/rerank, POST /v1/images/generations, POST /v1/audio/speech, POST /v1/audio/transcriptions, POST /v1/video/generations, POST /v1/inference/{model}, and catalog discovery on GET /v1/models and GET /v1/models/{model}. See budget semantics and remaining allowance for how DZD spend is tracked against budget.
Creating and using Scoped JWTs
Scoped JWTs are signed by DEVUP AI and can only be minted from your authenticated dashboard:
- Open Dashboard → API Keys, select the Scoped Tokens tab, and click Generate Scoped Token.
- Set an optional display name (up to 50 characters, not starting with
__), choose 1–50 allowed models (or enable Allow all models (*)), pick an expiration window (1 hour, 12 hours, 24 hours, 7 days, 30 days, or 90 days), and enter a spending limit between1and100,000DZD. - Copy the full
eyJ...token immediately. For security, the full token string is shown only once at creation; the dashboard stores only a masked preview, cumulative spend, allowed models, and expiration metadata.
Account-only endpoints: Scoped JWTs grant model and catalog access only. They cannot read your account profile or wallet balance: calling GET /v1/me or GET /v1/me/avatar with a Scoped JWT returns HTTP 403 scoped_token_not_allowed, and GET /v1/user/balance accepts sk-devup-... account keys only.
Token claims and permitted models
Every Scoped JWT is an HS256-signed compact JWS whose signature is verified on every request. Modifying any character of the header or payload invalidates the signature and returns HTTP 401 invalid_api_key. You can inspect a token's base64url-decoded claims client-side to check its model scope or expiration before sending a request:
Decoded JWS Header
{
"alg": "HS256"
}Decoded JWT Payload
{
"sub": "usr_01j8z9example",
"models": [
"deepseek-ai/DeepSeek-V4-Pro",
"openai/gpt-5.3-codex"
],
"budget": 250,
"cache_key": "8f14e45f-ceea-467a-9a38-7b1f09example",
"iat": 1761552000,
"exp": 1761638400
}| Claim | Type | Meaning |
|---|---|---|
| sub | string | Identifier of the DEVUP AI account that issued the token and funds its usage. |
| models | string[] | Canonical model IDs from GET /v1/models that this token may invoke, or ["*"] when all catalog models are permitted. |
| budget | number | Configured cumulative DZD spending limit for the token (1 to 100,000 DZD). |
| cache_key | string | Unique token identifier (UUID) used for instant revocation checks and cumulative spend tracking. |
| iat | number | Unix timestamp in seconds when the token was issued. |
| exp | number | Unix timestamp in seconds after which the token is rejected with HTTP 401 token_expired. |
Permitted model matching: Unless models contains "*", the model field in your request (or its resolved catalog ID from GET /v1/models) must appear in models. Requesting any other model returns HTTP 403 model_not_allowed before execution and is not billed. Each permitted model still enforces its own endpoint and capability rules published in GET /v1/models.
Token expiration and revocation
Every Scoped JWT has a mandatory expiration time (exp) between 1 hour and 90 days from creation. Once the expiration timestamp passes, all new requests with that token return HTTP 401 token_expired (for example, This scoped token expired at 2026-10-28T10:00:00.000Z.).
To cut off access before exp, click Revoke next to the token in Dashboard → API Keys → Scoped Tokens. Revocation takes effect immediately on subsequent requests, returning HTTP 401 token_revoked (This scoped token has been revoked.). Because claims are cryptographically signed at creation, neither exp, models, nor budget can be edited in place; issue a new Scoped JWT when rotating credentials or changing limits.
Budget semantics and remaining allowance
Each Scoped JWT tracks cumulative DZD spend attributed to that token alongside your account's DZD balance:
- Pre-request cumulative check: On every request, DEVUP AI checks the token's recorded DZD spend against its signed
budget. Once recorded spend reaches or exceedsbudget, new requests with that token return HTTP 402 budget_exhausted before execution. - Post-execution usage attribution: When a billed request completes, its verified DZD cost is deducted from your account balance and added to the token's cumulative spend counter shown in Dashboard → API Keys → Scoped Tokens.
- Concurrent requests and explicit output bounds: Because token spend is recorded upon request completion, multiple requests started in parallel before recorded spend reaches
budgetare each checked against the spend already recorded at admission. Pass conservativemax_completion_tokensormax_output_tokensvalues when callers run concurrent requests on a shared token.
Relationship between token limits and account balance
A Scoped JWT does not create a separate prefunded sub-wallet. All billed usage is deducted in Algerian dinars (DZD) from the issuing account's balance while also counting against the token's signed budget. Every request must pass both checks:
| Control | Scope | Error When Reached | How to Restore Access |
|---|---|---|---|
| Account DZD balance | Shared across all API keys and Scoped JWTs on your account | 402 insufficient_balance | Top up your balance in Dashboard → Billing |
| Scoped JWT spending limit | Cumulative recorded DZD spend attributed to that single eyJ... token | 402 budget_exhausted | Issue a new Scoped JWT in Dashboard → API Keys |
| Rate & concurrency limits | Account-wide requests/minute, tokens/minute, and active requests | 429 rate_limit_exceeded / concurrency_limit_exceeded | Wait for the Retry-After delay or finish open streams |
- Effective ceiling: Available spend for a request is bounded by whichever limit is lower—your available account DZD balance or the token's remaining DZD allowance. A token with a 1,000 DZD limit on an account holding 50 DZD stops with
insufficient_balanceonce the 50 DZD balance is used. - Top-ups do not reset token spend: Adding DZD to your account restores
insufficient_balancecapacity, whereas an exhausted Scoped JWT remains atbudget_exhaustedonce its settled spend reachesbudget. - Partial-balance fitting on text endpoints: On
POST /v1/chat/completionsandPOST /anthropic/v1/messages, a request withouttoolsis answered with a reducedmax_tokenswhen your account balance covers part of the reply (notice max_tokens_capped_by_balance). - Separate from rate limits: Waiting for a per-minute rate limit window never replenishes a token's DZD spending limit.
Chat Completions and Responses examples
Pass your Scoped JWT in Authorization: Bearer $SCOPED_JWT (or as apiKey / api_key in the official OpenAI SDKs) without changing any request or response schemas.
Chat Completions (POST /v1/chat/completions)
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["SCOPED_JWT"],
base_url="https://api.devupai.com/v1",
)
completion = client.chat.completions.create(
model="deepseek-ai/DeepSeek-V4-Pro",
messages=[
{"role": "system", "content": "Answer concisely."},
{"role": "user", "content": "Summarize three benefits of type-safe APIs."},
],
max_completion_tokens=512,
extra_headers={"x-client-request-id": "session-42-turn-1"},
)
print(completion.choices[0].message.content)Responses API (POST /v1/responses)
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["SCOPED_JWT"],
base_url="https://api.devupai.com/v1",
)
response = client.responses.create(
model="openai/gpt-5.3-codex",
input="Write a TypeScript helper that parses Retry-After seconds into milliseconds.",
max_output_tokens=600,
)
for item in response.output:
if item.type == "message":
for part in item.content:
if part.type == "output_text":
print(part.text)Streaming and tool calling
Scoped JWTs preserve full support for Server-Sent Events (stream: true), reasoning controls, structured outputs, and tool calling on every permitted model that supports those capabilities in GET /v1/models.
Streaming responses with x-request-id tracking
Capture the x-request-id response header when opening a stream. If a client disconnects or aborts mid-stream after generation has started, tokens already produced can still incur usage; quote x-request-id when reconciling an interrupted stream.
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["SCOPED_JWT"],
base_url="https://api.devupai.com/v1",
)
with client.chat.completions.with_streaming_response.create(
model="deepseek-ai/DeepSeek-V4-Pro",
messages=[{"role": "user", "content": "List five PostgreSQL indexing tips."}],
stream=True,
stream_options={"include_usage": True},
max_completion_tokens=512,
) as response:
print("Request ID:", response.headers.get("x-request-id"))
for chunk in response.parse():
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)Function & tool calling
Check supports.tools and tool_calls.chat_completions in GET /v1/models: models whose tool calling is served on /v1/responses return HTTP 400 use_responses_api if sent tools on /v1/chat/completions.
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["SCOPED_JWT"],
base_url="https://api.devupai.com/v1",
)
result = client.chat.completions.create(
model="deepseek-ai/DeepSeek-V4-Pro",
messages=[{"role": "user", "content": "What is the current weather in Algiers?"}],
tools=[
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Look up current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. Algiers"},
},
"required": ["city"],
"additionalProperties": False,
},
},
}
],
tool_choice="auto",
max_completion_tokens=256,
)
print(result.choices[0].message.tool_calls)Common Scoped JWT errors
Scoped JWT errors use the standard OpenAI-compatible JSON envelope (error.type = "devup_error", error.code, error.message) and include x-request-id. Match on error.code in client code:
| HTTP | Code | When Returned | Recommended Action |
|---|---|---|---|
| 400 | scoped_budget_unbounded_request | The request's maximum cost cannot be determined prior to execution under a finite token budget (prompt-only per-second video generation, token-metered image models, per-second or webhook /v1/inference calls, non-image chat media parts, or unrecognized media containers). | Use a supported bounded model or container format, or authenticate with an sk-devup-... account API key. |
| 401 | missing_api_key | No Authorization: Bearer (or x-api-key where supported) header was sent. | Send Authorization: Bearer $SCOPED_JWT. |
| 401 | invalid_api_key | The JWT is malformed, truncated, altered, missing required claims, or unknown. | Do not retry; verify the exact token string or generate a fresh token. |
| 401 | token_expired | The token's exp timestamp has passed. | Mint a new Scoped JWT from your server/dashboard before exp. |
| 401 | token_revoked | The token was revoked in Dashboard → API Keys → Scoped Tokens. | Do not retry; issue a replacement token if access is still intended. |
| 402 | budget_exhausted | Settled spend reached budget, or remaining allowance across active concurrent requests does not cover the request's maximum cost. | Lower explicit output limits, wait for active calls on the token to settle if allowance remains, or issue a new Scoped JWT. |
| 402 | insufficient_balance | The owning account's DZD wallet balance cannot cover the request. | Top up your account DZD balance in Dashboard → Billing, or lower output token limits. |
| 403 | model_not_allowed | The requested model is not in the token's models allowlist. | Switch to one of the allowed models listed in the error message, or issue a token covering that model. |
| 403 | scoped_token_not_allowed | A Scoped JWT was sent to GET /v1/me or GET /v1/me/avatar. | Use an account API key (sk-devup-...) for account profile endpoints. |
| 403 | account_suspended | The owning DEVUP AI account is suspended. | Contact support@devupai.com. |
| 409 | request_outcome_pending | A prior attempt with the same request identifier is still awaiting outcome confirmation. | Do not automatically replay the same operation; retain x-request-id. |
| 429 | rate_limit_exceeded | Account rate, TPM, or concurrency limit (or model_at_capacity) was reached. | Wait for the Retry-After header delay before retrying. |
| 503 | service_unavailable | Request limits or credential state cannot be verified momentarily, or maintenance is active. | Respect Retry-After (5 seconds) with bounded backoff; confirm outcome before repeating non-idempotent work if instructed by the message. |
Security best practices
- Prefer explicit model allowlists over
*: Grant only the exact models a workflow needs so a leaked token cannot invoke higher-cost text, image, audio, or video models. - Issue short lifetimes for untrusted environments: Choose 1-hour or 24-hour expirations when handing tokens to browser sessions, CI jobs, or third-party previews, and rotate tokens before
exp. - Bound per-request output tokens: Always pass
max_completion_tokensormax_output_tokensalongside conservative DZDbudgetvalues, especially when callers can submit concurrent requests. - Never commit credentials to source control: Store tokens in environment variables or a secret manager, and revoke any exposed token immediately in Dashboard → API Keys → Scoped Tokens.
- Correlate requests with
x-client-request-id: Send a per-operationx-client-request-id(1–64 characters ofA-Za-z0-9._:-) and log the returnedx-request-idso usage and errors can be traced cleanly.
Compatibility and migration notes
- No credential rotation required: Existing
sk-devup-...API keys and existingeyJ...Scoped JWTs remain valid byte-for-byte with their current signatures,cache_keyidentifiers, model allowlists, expiration timestamps, and prior recorded spend. - Drop-in SDK compatibility: Pass a Scoped JWT wherever the OpenAI SDK expects
apiKey/api_key(withbaseURL: "https://api.devupai.com/v1") or wherever the Anthropic SDK expectsapiKey/authTokenonhttps://api.devupai.com/anthropic. No request parameter changes are required. - Forward-compatible error handling: Match programmatically on
error.code, honorRetry-Afteron HTTP 429 and HTTP 503 responses, do not retry HTTP 400scoped_budget_unbounded_requestor HTTP 401/403 credential rejections with the same request, and do not blindly replay HTTP 409request_outcome_pendingwithout checking the prior request's outcome.
Account profile
Read the profile of the account that owns the API key: a display name, an initial, a masked email hint and the profile picture. Tools such as editor extensions use it to show the signed-in account. The full email address is never returned.
Authentication: Send your API key as Authorization: Bearer <key> or in the x-api-key header. When both are sent, Authorization is used. A missing or invalid key returns the same errors as GET /v1/models, for example HTTP 401 missing_api_key. Both endpoints accept account API keys (sk-devup-...) only: a scoped token returns HTTP 403 scoped_token_not_allowed.
Caching: GET /v1/me is sent with Cache-Control: private, max-age=300. GET /v1/me/avatar is sent with Cache-Control: private, max-age=86400 and an ETag equal to avatar.etag; send it in If-None-Match to receive 304 Not Modified with no body.
Billing and limits: Both requests are free of charge and count against your account's per-minute rate limit.
GET /v1/me response fields
The response contains exactly these fields and no others.
| Field | Type | Description |
|---|---|---|
| object | string | Always "account_profile". |
| display_name | string | null | The account's name, trimmed, with control and invisible characters removed, at most 64 characters. null when the account has no name. |
| initial | string | One uppercase letter or digit: from display_name, else from the local part of the email, else "?". |
| email_hint | string | null | The first character of the email's local part, then •••, @ and the domain. null when the email is not known. |
| avatar | object | null | null when the account has no profile picture. Otherwise url, the absolute URL of GET /v1/me/avatar, and etag, the entity tag of the current picture. |
GET /v1/me/avatar
- Returns the image bytes directly in the response body, with
Content-Typeexactlyimage/png,image/jpegorimage/webp. - The image is square, at most 256×256 pixels and at most 256 KB (256,000 bytes).
- An account with no profile picture returns HTTP 404 avatar_not_found with the message
This account has no profile picture.In that caseGET /v1/meshowsavatar: null. - While the picture is temporarily unavailable, the response is HTTP 503 service_unavailable with a
Retry-Afterheader; request it again after that delay. During that timeGET /v1/mecan showavatar: null.
Example
Read the account profile:
curl https://api.devupai.com/v1/me \
-H "Authorization: Bearer $DEVUP_API_KEY"Example response:
{
"object": "account_profile",
"display_name": "Ada Lovelace",
"initial": "A",
"email_hint": "a•••@example.com",
"avatar": {
"url": "https://api.devupai.com/v1/me/avatar",
"etag": "\"3f2a9c1b7d4e6a58\""
}
}Download the profile picture, sending the etag of a copy you already have:
curl https://api.devupai.com/v1/me/avatar \
-H "Authorization: Bearer $DEVUP_API_KEY" \
-H 'If-None-Match: "3f2a9c1b7d4e6a58"' \
-o avatar.img