Guides
Headers & response shapes
The request headers Mahvash understands, the two response formats, pagination, error objects, retries and idempotency — everything a robust client needs to handle every call the same way.
Request headers
These headers mean the same thing on every endpoint that accepts them:
| Header | When to send | Meaning |
|---|---|---|
Authorization | Every authenticated request | Bearer <project API key> or Bearer <user JWT>, as the endpoint requires |
Content-Type | Requests with a body | application/json, or multipart/form-data generated by your HTTP library for uploads |
X-Mahvash-Organization | User-JWT requests outside your personal organization | Organization UUID or slug among your active memberships |
X-Mahvash-Project | User-JWT requests outside the default project | Project UUID or slug you can reach |
X-Request-ID | Recommended on every request | Your correlation ID, echoed back on every response |
Idempotency-Key | Paid AI creation requests you might retry | Replays a finished result instead of running the work twice |
Accept: text/event-stream | Streaming Responses or Chat Completions | Makes the streaming intent explicit; stream: true controls the behaviour |
A fully decorated request looks like this:
curl https://api.mahvashai.ir/v1/responses \
-H "Authorization: Bearer $MAHVASH_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "X-Request-ID: checkout-7f3a91" \
-H "Idempotency-Key: order-1042-summary-v1" \
-d '{"model": "auto", "input": "Summarize order 1042", "stream": true}'- For JSON bodies send
Content-Type: application/json. For uploads let your HTTP library buildmultipart/form-data— never set the boundary by hand. X-Request-IDis optional. When you send 1–128 characters ofA-Za-z0-9._:-, Mahvash uses your value; otherwise it generates one (req_...). Either way, the value comes back in theX-Request-IDresponse header on success, on error and on streams, so you can join your logs with Mahvash's.
Two response shapes
The URL tells you how to read the body:
| Surface | Success body | Error body | Lists |
|---|---|---|---|
/v1/* | The object itself | { "error": { message, type, param, code } } | Endpoint-specific; commonly data, first_id, last_id, has_more |
/api/v1/* | { "status", "data" } | { "status", "data" } with details in data | Endpoint-specific inside data; commonly page / page_size |
Binary or 204 | Raw bytes or empty | Status code | Not applicable |
{
"id": "resp_<uuid>",
"object": "response",
"status": "completed",
"output": []
}/v1/*endpoints return the object directly, exactly as an OpenAI SDK expects./api/v1/*endpoints wrap the payload as{ "status": <http status>, "data": ... }.statusmirrors the HTTP status; your data is always insidedata, both on success and on error.- Binary downloads (file content, generated audio) and
204 No Contentresponses have no JSON body at all.
Note:Write one small helper per surface — unwrap() for /api/v1 and a direct reader for /v1 — instead of guessing per call.
Pagination
Lists do not share one universal shape, so always read the pagination fields of the endpoint you are calling. Two patterns are common:
- Cursor lists on
/v1returnobject: "list",data,first_id,last_idandhas_more. Request the next page by passing the last ID you saw asafter(orbeforeto go backwards), withlimitcontrolling the page size. - Page lists on
/api/v1acceptpageandpage_sizeand return the items and page information inside the envelope'sdata.
# First page
curl "https://api.mahvashai.ir/v1/responses/resp_<uuid>/input_items?limit=20" \
-H "Authorization: Bearer $MAHVASH_API_KEY"
# {"object": "list", "data": [...], "first_id": "...", "last_id": "msg_x", "has_more": true}
# Next page: pass the previous last_id as "after"
curl "https://api.mahvashai.ir/v1/responses/resp_<uuid>/input_items?limit=20&after=msg_x" \
-H "Authorization: Bearer $MAHVASH_API_KEY"Stop when has_more is false or when a page comes back empty. Do not assume a fixed default page size, and do not treat IDs from one list as cursors for another.
Errors
/v1 endpoints report failures with a stable error object that always has four keys. /api/v1 endpoints put their validation details inside the envelope's data:
{
"error": {
"message": "The selected model does not support the requested capabilities or limits.",
"type": "invalid_request_error",
"param": null,
"code": "invalid_request"
}
}The HTTP status tells you what kind of failure happened:
| HTTP | Typical codes | What it means | What to do |
|---|---|---|---|
| 400 | invalid_request | Unknown field, wrong type or range, or a capability the model does not support | Fix the request; do not retry unchanged |
| 401 | invalid_api_key | Missing, expired, revoked or invalid token | Refresh the JWT or check the key |
| 402 | insufficient_balance | The organization cannot pay for the request | Top up or lower the request's cost |
| 403 | permission_denied | Authenticated, but not allowed to do this | Use a key or role with the needed permission |
| 404 | not_found, response_not_found | The resource does not exist or is outside your scope | Check the ID and the selected project |
| 409 | idempotency_key_reused, idempotent_request_in_progress, idempotent_stream_not_replayable | An idempotency or state conflict | Use a new key for new work, or retrieve the result |
| 429 | rate_limit_exceeded, quota_exceeded, active_run_limit | A rate, quota or concurrency limit was reached | Back off; honour Retry-After |
| 502 | provider_outcome_unknown, invalid_structured_output | The provider failed, broke the requested format, or may have processed the request | Do not blindly retry an unknown outcome; inspect it |
| 503 | provider_unavailable, feature_disabled | No eligible route, or a required service is unavailable | Retry later or change model and options |
| 500 | internal_error | Unexpected server failure | Retry with backoff; report the X-Request-ID |
On /v1, error.type is derived from the status and is the field to branch on: invalid_request_error is permanent (fix the request), while rate_limit_error and server_error are transient (back off and retry). error.code gives the specific reason; codes are stable, but new ones can appear, so always keep a default branch.
Retrying safely
Most AI calls cost money, so a retry policy has to avoid paying twice for the same work:
- Retry
429and5xxwith exponential backoff and jitter. WhenRetry-Afteris present, wait at least that many seconds. - Do not retry other
4xxerrors unchanged — they will fail the same way. - Never blindly retry
provider_outcome_unknown, a client timeout, or a dropped connection after the request was sent. The provider may already have done the work. Look up the response by its ID (or replay with the same idempotency key) before deciding. - Reuse the same
Idempotency-Keyon every retry of one logical operation. A new key means a new, separately charged request.
const RETRYABLE = new Set(["rate_limit_error", "server_error"]);
export async function callMahvash(path, body, { idempotencyKey, attempts = 3 } = {}) {
for (let attempt = 1; ; attempt++) {
const response = await fetch("https://api.mahvashai.ir" + path, {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.MAHVASH_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey, // the SAME key on every attempt
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(60_000),
});
if (response.ok) return response.json();
const requestId = response.headers.get("X-Request-ID");
const { error } = await response.json().catch(() => ({ error: {} }));
const ambiguous = error?.code === "provider_outcome_unknown";
if (!RETRYABLE.has(error?.type) || ambiguous || attempt >= attempts) {
throw Object.assign(new Error(error?.message ?? "Mahvash error"), {
status: response.status, code: error?.code, requestId,
});
}
const retryAfter = Number(response.headers.get("Retry-After"));
const delay = retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt + Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}Important:Disable automatic retries in HTTP libraries and SDKs (for example maxRetries: 0) unless you have confirmed they reuse your idempotency key. Two layers of retries multiply each other.
Idempotency keys
An Idempotency-Key turns "did my request go through?" into a safe question. It is accepted by the AI creation endpoints — Responses, Chat Completions, embeddings, images, audio, moderations, search and batches. The reference lists it among each endpoint's headers.
- The key is 1–255 visible ASCII characters. Build it from your own identity for the operation, such as
ticket-812-reply-v3, or a UUID stored before the first attempt. - Keys are scoped to the project and the API operation and remembered for 24 hours by default.
# First call: runs the model once and stores the result
curl https://api.mahvashai.ir/v1/responses \
-H "Authorization: Bearer $MAHVASH_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-8812-summary" \
-d '{"model": "auto", "input": "Summarize invoice 8812"}'
# Same key + same body within the window: the stored response is replayed,
# no second model call and no second charge.
# Same key + different body: 409 idempotency_key_reusedWhat happens when a key is reused:
- Same key, same request, finished successfully (non-streaming): the stored response is returned again. No new model call, no new charge.
- Same key, different request body:
409 idempotency_key_reused. - Same key while the first request is still running:
409 idempotent_request_in_progress. Wait and try again, or retrieve the response. - Streaming requests are never replayed:
409 idempotent_stream_not_replayable. Retrieve the stored response instead. - A request that failed or timed out keeps its key reserved until expiry, because its upstream outcome may be unknown.