Guides
Authentication
Mahvash uses two kinds of bearer token. Learn what each one represents, which endpoints accept it, how to issue and rotate project keys, and how to keep secrets out of reach.
Two credential types
Every authenticated request sends Authorization: Bearer <token>. The token is one of two very different things:
| Credential | Represents | Best for | Scope and lifetime |
|---|---|---|---|
Project API key (mk_...) | Standing access for a system, acting with its issuer's permissions | Server-to-server AI requests | Pinned to one organization and project; optional scopes and expiry; shown once |
| User access JWT | A signed-in person | Workspace setup and administration, issuing keys | The person's active memberships and roles; short-lived, renewed with a refresh token |
The rule of thumb: your software uses project API keys; people use JWTs. A background service that drafts support replies should hold a project key. An administrator creating that service's project and key does so as themselves, with a JWT, so the audit trail names them.
Each endpoint in the API reference states which credential it accepts. AI endpoints under /v1 accept either; workspace management under /api/v1/tenancy and webhook management require a user JWT.
User access tokens
People sign in with their phone number and a one-time code. A successful verification returns an access token and a refresh token:
# 1. Ask for a one-time code (sent by SMS to the phone number)
curl -X POST https://api.mahvashai.ir/api/v1/users/register/ \
-H "Content-Type: application/json" \
-d '{"phone": "09XXXXXXXXX"}'
# 2. Verify the code and receive a token pair
curl -X POST https://api.mahvashai.ir/api/v1/users/register/auth/ \
-H "Content-Type: application/json" \
-d '{"phone": "09XXXXXXXXX", "otp": "<code>"}'The first successful sign-in for a phone number creates the account (created: true) together with a personal organization and a default project, so the new user can make requests immediately.
Access tokens expire. When a request answers 401, exchange the refresh token for a new pair:
curl -X POST https://api.mahvashai.ir/api/v1/users/refresh/ \
-H "Content-Type: application/json" \
-d '{"refresh": "<user-refresh-jwt>"}'- A refresh token is single-use: store the new pair and discard the old one.
- Only a
401from the refresh endpoint means the session is over. A409 refresh_token_rotatedmeans another request already rotated the same token moments ago; a429or503is temporary. In those cases keep the tokens and retry shortly. - Refresh from one place at a time. Two concurrent refreshes with the same token cannot both win.
Note:Most integrations only need a JWT during setup — to create projects and issue keys. Your production AI traffic should then use the project key, not a person's token.
Issuing a project API key
Keys are issued for a specific project by a person who is an administrator of it. You can do it in the dashboard or through the API:
curl -X POST https://api.mahvashai.ir/api/v1/tenancy/projects/<project-uuid>/credentials \
-H "Authorization: Bearer $MAHVASH_USER_JWT" \
-H "X-Mahvash-Organization: acme" \
-H "Content-Type: application/json" \
-d '{
"name": "production-backend",
"scopes": ["models.request", "chat.read"],
"expires_at": "2027-06-30T00:00:00Z"
}'name(required) is a human label. Name keys after the system and environment that use them.scopesnarrows what the key may do. Omit it or send[]for everything the project allows.expires_atmakes the key stop working at a fixed time — useful for contractors or temporary tests.attributed_teamcharges the key's usage to a team for reporting. It is a label only and grants no access.
The response contains token exactly once. Copy it straight into your secret store. The prefix (for example mk_1a2b3c4d5e6f) is the public half: safe to log and enough to identify or revoke the key later.
Then send the key as a bearer token:
curl https://api.mahvashai.ir/v1/responses \
-H "Authorization: Bearer $MAHVASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "auto", "input": "Ping"}'Scopes: least privilege for keys
Scopes limit a key to the operations a particular system needs. A key that only drafts replies does not need to delete files.
| Scope | Permits |
|---|---|
models.request | Creating responses, chat completions, images, audio, embeddings and moderations |
chat.read / chat.write | Reading conversations and runs / cancelling and deleting them |
files.read / files.write | Reading assets and uploads / creating and deleting them |
vector_stores.read / vector_stores.write | Reading stores and their files / creating, updating, deleting and moving them |
prompts.read / prompts.write | Reading project prompts / editing them |
projects.read | Reading the selected project and organization |
usage.read | Reading this account's own consumption |
- A
*.writescope includes its matching*.readscope, so a key that creates vector stores can read them back. models.requestdoes not include reading history. A key that sends prompts and reads stored conversations needsmodels.requestandchat.read.- An unknown scope is rejected when the key is issued, never silently ignored — a typo can never produce a key with more reach than you intended.
What a key can never do
Whatever its scopes and whoever created it, a project key cannot perform organization administration. The following are refused to every key:
- inviting, removing or changing the role of members;
- managing teams and project access grants;
- issuing, rotating or revoking API keys;
- managing webhook subscriptions;
- reading the audit trail or organization-wide usage reports, or anything related to the wallet.
A key also acts with the live permissions of the person who issued it, in its project — never more. If that person's role is lowered, the key's reach shrinks with it. If that person is removed from the organization or suspended, every key they issued is revoked in the same moment, and it stays revoked even if they later rejoin. Plan for this: when a teammate who owns an integration leaves, issue a replacement key from another administrator before removing them.
Finally, a key is pinned: it always works in its own organization and project. A request that sends a different X-Mahvash-Organization or X-Mahvash-Project is refused instead of being quietly redirected.
Rotation and revocation
Treat keys like passwords with a lifecycle:
# Planned replacement: the old key keeps working during the overlap window
curl -X POST https://api.mahvashai.ir/api/v1/tenancy/credentials/<credential-uuid>/rotate \
-H "Authorization: Bearer $MAHVASH_USER_JWT" \
-H "X-Mahvash-Organization: acme"
# Suspected leak: stop the key immediately
curl -X DELETE https://api.mahvashai.ir/api/v1/tenancy/credentials/<credential-uuid> \
-H "Authorization: Bearer $MAHVASH_USER_JWT" \
-H "X-Mahvash-Organization: acme"- Rotate for planned replacement. You receive a new token, and the old one keeps working for an overlap window (24 hours by default) so you can deploy the new key without downtime.
- Revoke when a key may have leaked. It stops working immediately.
Revoked keys remain visible in the key list so an investigation can see when a key existed and when it was stopped. To know whether a key works right now, read its usable field: a key can be ACTIVE and already expired.
Store credentials safely
- Keep keys and tokens on trusted servers only, in environment variables or a secret manager.
- Never put a key or token in browser code, mobile app packages, URLs or query strings, logs, analytics events, screenshots, or public repositories.
- Use one key per system and environment, so you can revoke one without breaking the rest.
- Give each key the smallest set of scopes it needs and an expiry when the use is temporary.
- Log the
X-Request-IDand the key prefix when you need to trace a call — never the key itself.
Important:If a key is exposed, revoke it first and investigate second. A rotated key keeps working during its overlap window, so rotation is not the right response to a leak.