Skip to content

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:

HeaderWhen to sendMeaning
AuthorizationEvery authenticated requestBearer <project API key> or Bearer <user JWT>, as the endpoint requires
Content-TypeRequests with a bodyapplication/json, or multipart/form-data generated by your HTTP library for uploads
X-Mahvash-OrganizationUser-JWT requests outside your personal organizationOrganization UUID or slug among your active memberships
X-Mahvash-ProjectUser-JWT requests outside the default projectProject UUID or slug you can reach
X-Request-IDRecommended on every requestYour correlation ID, echoed back on every response
Idempotency-KeyPaid AI creation requests you might retryReplays a finished result instead of running the work twice
Accept: text/event-streamStreaming Responses or Chat CompletionsMakes 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 build multipart/form-data — never set the boundary by hand.
  • X-Request-ID is optional. When you send 1–128 characters of A-Za-z0-9._:-, Mahvash uses your value; otherwise it generates one (req_...). Either way, the value comes back in the X-Request-ID response 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:

SurfaceSuccess bodyError bodyLists
/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 dataEndpoint-specific inside data; commonly page / page_size
Binary or 204Raw bytes or emptyStatus codeNot 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": ... }. status mirrors the HTTP status; your data is always inside data, both on success and on error.
  • Binary downloads (file content, generated audio) and 204 No Content responses 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 /v1 return object: "list", data, first_id, last_id and has_more. Request the next page by passing the last ID you saw as after (or before to go backwards), with limit controlling the page size.
  • Page lists on /api/v1 accept page and page_size and return the items and page information inside the envelope's data.
# 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:

HTTPTypical codesWhat it meansWhat to do
400invalid_requestUnknown field, wrong type or range, or a capability the model does not supportFix the request; do not retry unchanged
401invalid_api_keyMissing, expired, revoked or invalid tokenRefresh the JWT or check the key
402insufficient_balanceThe organization cannot pay for the requestTop up or lower the request's cost
403permission_deniedAuthenticated, but not allowed to do thisUse a key or role with the needed permission
404not_found, response_not_foundThe resource does not exist or is outside your scopeCheck the ID and the selected project
409idempotency_key_reused, idempotent_request_in_progress, idempotent_stream_not_replayableAn idempotency or state conflictUse a new key for new work, or retrieve the result
429rate_limit_exceeded, quota_exceeded, active_run_limitA rate, quota or concurrency limit was reachedBack off; honour Retry-After
502provider_outcome_unknown, invalid_structured_outputThe provider failed, broke the requested format, or may have processed the requestDo not blindly retry an unknown outcome; inspect it
503provider_unavailable, feature_disabledNo eligible route, or a required service is unavailableRetry later or change model and options
500internal_errorUnexpected server failureRetry 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 429 and 5xx with exponential backoff and jitter. When Retry-After is present, wait at least that many seconds.
  • Do not retry other 4xx errors 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-Key on 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_reused

What 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.