# MCP Server Source: https://developers.meshapi.ai/agent/mcp Connect any MCP-compatible agent or client to Mesh and call models, tools, RAG, and more — authenticated with your Mesh API key. Mesh runs a built-in **[Model Context Protocol (MCP)](https://modelcontextprotocol.io)** server, so any MCP-compatible agent or client — Claude Code, Claude Desktop, Cursor, or your own agent framework — can use Mesh's models and tools directly, without writing any Mesh-specific HTTP code. The server exposes the Mesh API surface as MCP tools: an agent can run chat completions, reason with the Responses API, generate and edit images, create embeddings, moderate content, ask an evaluation model typed questions, search the web, query your RAG files, manage prompt templates, and check your balance. Every tool call authenticates with your existing Mesh API key. The MCP server is a thin layer over the same routes as the REST API. Every tool call runs the identical authentication, rate-limit, spend-cap, balance, and usage-logging pipeline as a direct API call, and is billed to the same key. ## Connection details | | | | - | - | | **Endpoint** | `https://api.meshapi.ai/mcp/` (with the trailing slash) | | **Transport** | Streamable HTTP | | **Authentication** | `Authorization: Bearer rsk_...` (your Mesh API key) | `https://api.meshapi.ai/mcp` without the slash answers with a `307` redirect to `/mcp/`. Use the trailing-slash URL so a client that doesn't follow redirects on `POST` still connects. You connect with the **same `rsk_` API key** you use for the REST API — no separate credential or OAuth flow. Pass it in the `Authorization` header when registering the server with your client. ## Connect a client ```bash theme={null} claude mcp add --transport http mesh-api https://api.meshapi.ai/mcp/ \ --header "Authorization: Bearer rsk_YOUR_KEY" ``` Then, in a session, ask Claude to use the Mesh tools (e.g. "use mesh-api to draft a reply with `openai/gpt-4o-mini`"). Add Mesh to your client's MCP config (`claude_desktop_config.json` for Claude Desktop, `.cursor/mcp.json` for Cursor): ```json theme={null} { "mcpServers": { "mesh-api": { "type": "http", "url": "https://api.meshapi.ai/mcp/", "headers": { "Authorization": "Bearer rsk_YOUR_KEY" } } } } ``` Restart the client; the Mesh tools appear in the tool picker. ```python theme={null} import asyncio from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client async def main(): async with streamablehttp_client( "https://api.meshapi.ai/mcp/", headers={"Authorization": "Bearer rsk_YOUR_KEY"}, ) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() # Discover the available tools tools = await session.list_tools() print([t.name for t in tools.tools]) # Call one result = await session.call_tool( "chat", { "model": "anthropic/claude-haiku-4.5", "messages": [{"role": "user", "content": "Say hi in one word."}], }, ) print(result.structuredContent) asyncio.run(main()) ``` Keep your `rsk_` key secret — it authorizes billable calls against your account. Treat an MCP client config that embeds the key like any other secret store, and rotate the key from the dashboard if it leaks. ## Available tools Tools mirror the REST API one-to-one. Inputs are OpenAI-shaped, so anything you know from the [API reference](/docs/reference/api-overview) carries over directly. ### Inference | Tool | What it does | Billed | | - | - | - | | `chat` | Chat completion against any catalog model (`model`, `messages`, optional `temperature`, `max_tokens`) | Yes | | `responses` | Responses API for reasoning models / hosted tools (`model`, `input`, `max_output_tokens`, `reasoning_effort`: `minimal`, `low`, `medium`, or `high`) | Yes | | `embeddings` | Embedding vectors for one or more texts (`model`, `input`) | Yes | | `moderations` | Classify text for policy violations (`input`, optional `model`) | Yes | | `evaluate` | Ask an evaluation model typed questions about application state and get probabilities back (`model`, `state`, `questions`). See [Evaluate](/docs/capabilities/evaluate) | Yes | | `compare` | Run the same messages across several models (`models`, `messages`, optional `comparison_model`, `temperature`) | Yes | | `router_select` | Ask the Auto Router which model best fits (`messages`, optional `candidate_models`, `exclude_models`). No completion is run | No | | `generate_image` | Generate image(s) from a prompt (`prompt`, `model`, `n`, `size`, `response_format`) | Yes | | `edit_image` | Edit an image with a prompt (`model`, `prompt`, `image`, optional `mask`, `n`, `size`, `response_format`). `image` and `mask` are data URLs or base64; remote URLs are rejected | Yes | ### RAG & search | Tool | What it does | Billed | | - | - | - | | `web_search` | Web search with ranked results (`query`, `max_results`, `search_depth`) | Yes | | `file_search` | Semantic search over your uploaded files (`query`, `top_k`, optional `file_ids`) | Yes | | `upload_file` | Upload a document to RAG (`file_name`, `mime_type`, `content_base64`, `embed`); embedding runs asynchronously | No | | `list_files` | List your uploaded RAG files | No | | `get_file` | Status / metadata for one file (`file_id`) — poll until `embedding_status` is ready | No | ### Catalog & account | Tool | What it does | Billed | | - | - | - | | `list_models` | List catalog models, optionally filtered by `provider` | No | | `list_voices` | List text-to-speech voices available to your key | No | | `get_balance` | Current credit balance for your account | No | ### Prompt templates | Tool | What it does | Billed | | - | - | - | | `list_templates` | List prompt templates for your org | No | | `get_template` | Get one template (`template_id`) | No | | `create_template` | Create a template (`name`, optional `system`, `messages`, `model`, `description`, `variables`) | No | | `update_template` | Update a template (`template_id` + fields to change) | No | | `delete_template` | Delete a template (`template_id`) | No | The server also exposes a **resource**, `mesh://models`, that returns the full model catalog as JSON — useful for clients that consume MCP resources rather than calling `list_models`. ## Example: give an agent a document Q\&A tool A typical agent flow — upload a file, wait for embeddings, then answer questions grounded in it — is three tool calls: Call `upload_file` with the file's bytes base64-encoded. You get back a `file_id`. ```json theme={null} { "name": "upload_file", "arguments": { "file_name": "handbook.pdf", "mime_type": "application/pdf", "content_base64": "JVBERi0xLjc...", "embed": true } } ``` Poll `get_file` with the `file_id` until `embedding_status` is `ready`. Embedding is asynchronous, so a freshly uploaded file is not immediately searchable. Call `file_search` with a natural-language `query`, then feed the returned passages into `chat` to get a grounded answer. ## Authentication & billing * **Auth:** the `Authorization: Bearer rsk_...` header is validated before any tool runs. A missing or invalid key returns **401**; a suspended key returns **403**. * **Limits & billing:** billed tool calls inherit the key's per-key rate limits, spend cap, model allow-list, and credit balance — exactly as a direct REST call. If a call would exceed a limit or your balance is insufficient, the tool returns an error carrying the Mesh message (e.g. *"Insufficient balance."*). * **Free tools:** the catalog/account reads (`list_models`, `list_voices`, `get_balance`), the RAG file reads (`list_files`, `get_file`, `upload_file`), template management, and `router_select` are not billed. Everything under **Inference**, plus `web_search` and `file_search`, is billed. * **Usage tracking:** every billed tool call is logged to your usage history just like a REST request, so MCP traffic shows up in the dashboard and usage reports. ## Limitations Tool results are returned as a single payload — there is no token-by-token streaming of a `chat`/`responses` result over MCP. For streaming, use the REST API or an SDK. The following Mesh capabilities are **not yet exposed as MCP tools** — use the [REST API](/docs/reference/api-overview) or an SDK for them: * Audio: text-to-speech and speech-to-text (transcription/translation) * Video generation and the async Batch API * Realtime audio (bidirectional WebSocket) * Usage-report queries ## Troubleshooting | Symptom | Likely cause / fix | | - | - | | `401 Unauthorized` when connecting | Missing or malformed `Authorization` header. It must be `Bearer rsk_...`. | | `403 Forbidden` | The key is suspended — check it in the dashboard. | | Tool returns *"Insufficient balance."* | Add credits, or the call exceeded your spend cap. | | `file_search` returns nothing for a just-uploaded file | Embedding is still processing — poll `get_file` until `embedding_status` is `ready`. | | A model isn't callable | It may be outside your key's model allow-list, or not in the catalog — check `list_models`. | # Create Admin Key Source: https://developers.meshapi.ai/api/admin-keys/create-admin-key /api/openapi.json post /v1/admin-keys Mint an admin key. The plaintext is returned exactly once. D4 rule 1: the caller may grant only permissions they already hold, at a scope they already hold — checked against `check_org_permissions`'s resolved `OrgAccess`, never against the token's own claims. # Get Admin Key Source: https://developers.meshapi.ai/api/admin-keys/get-admin-key /api/openapi.json get /v1/admin-keys/{key_id} # List Admin Keys Source: https://developers.meshapi.ai/api/admin-keys/list-admin-keys /api/openapi.json get /v1/admin-keys # Revoke Admin Key Source: https://developers.meshapi.ai/api/admin-keys/revoke-admin-key /api/openapi.json delete /v1/admin-keys/{key_id} Revoke immediately — no grace window (§7). Re-checks current authority against the key's own scope, like rotate. Revocation is destructive: a demoted admin who can still SEE a key they created must not be able to kill the org's automation with it. # Rotate Admin Key Source: https://developers.meshapi.ai/api/admin-keys/rotate-admin-key /api/openapi.json post /v1/admin-keys/{key_id}/rotate Mint a successor, then shorten the predecessor to a grace window (§7). Both keys work during the overlap — rotation is not revocation, which is immediate and has no grace. Re-checks the caller's CURRENT authority against the predecessor's own scope/permissions, exactly like create — the route floor alone (default SELF) is not enough: a caller demoted since minting an ORG-scoped key would otherwise still find it via `created_by` and rotate it into a fresh 90-day ORG-scoped credential nobody would let them create today. # List dated API versions Source: https://developers.meshapi.ai/api/api-versions/list-dated-api-versions /api/openapi.json get /v1/api-versions Every dated version this deployment serves, oldest first. Send one as `X-Mesh-Version` to pin a request, or store it on an API key to pin every request that key makes. Omit it and you get the `baseline` entry. An unsupported or malformed value is rejected with `400 invalid_api_version` rather than falling back silently, so a typo cannot leave you believing you are pinned when you are not. # Create Speech Source: https://developers.meshapi.ai/api/audio/create-speech /api/openapi.json post /v1/audio/speech Convert text to speech. Provider is resolved automatically from the model name via the model_pricing table — no hardcoded provider branching. Streaming is used when the provider adapter supports it and `stream=true` (the default). The `voice` field is required for ElevenLabs models. Sarvam models use `speaker` instead. # Create Transcription Source: https://developers.meshapi.ai/api/audio/create-transcription /api/openapi.json post /v1/audio/transcriptions Transcribe an audio file to text. Provider is resolved automatically from the model name via model_pricing. `file` is optional for providers that accept `source_url` or `cloud_storage_url`. `additional_formats` is a JSON-encoded string (ElevenLabs-specific, ignored by others). Returns `{"text": "..."}` by default, or per-word structure (`words[]` with each token's `type`) when `response_format=verbose_json`. # Create Transcription Translate Source: https://developers.meshapi.ai/api/audio/create-transcription-translate /api/openapi.json post /v1/audio/transcriptions/translate Transcribe audio and translate it to English. **Legacy alias** for `POST /v1/audio/translations` — prefer that OpenAI-standard path. Behaviour is identical; retained so existing callers keep working. # Create Translation Source: https://developers.meshapi.ai/api/audio/create-translation /api/openapi.json post /v1/audio/translations Translate speech in any language to English text. OpenAI-compatible `POST /v1/audio/translations` — the path the OpenAI SDK's `client.audio.translations.create()` calls. Provider is resolved from the model name via model_pricing; the adapter must implement `speech_to_text_translate()` (providers that don't → 422). `response_format`: `json` (default) → `{"text": "..."}`; `text` → the raw transcript as `text/plain`; `verbose_json` → the per-word structure. All in English. # Get Voice Source: https://developers.meshapi.ai/api/audio/get-voice /api/openapi.json get /v1/audio/voices/{voice_id} Return metadata for a single ElevenLabs voice by ID. # List Voices Source: https://developers.meshapi.ai/api/audio/list-voices /api/openapi.json get /v1/audio/voices List available TTS voices across every model brand, filterable by `brand`. A unified, normalised catalog covering ElevenLabs, Cartesia, Kokoro, Orpheus, and other TTS model brands. ElevenLabs and dynamic (e.g. Cartesia) voices are fetched live; open-weight preset voices (Kokoro / Orpheus) come from a static catalog. Filters (case-insensitive): `brand` (e.g. `elevenlabs`, `hexgrad`, `canopylabs`, `cartesia`), `model` (canonical model_id), and `search` (substring match on voice id / name). Per-brand fetches are best-effort — a brand that errors is skipped rather than failing the whole listing. # Export Audit Logs Csv Source: https://developers.meshapi.ai/api/audit-logs/export-audit-logs-csv /api/openapi.json get /v1/audit-logs.csv Same dependency as the JSON route, so the role gate cannot drift: a member who cannot list entries cannot download them either. assert_audit_readable() runs HERE, synchronously, before the response is constructed — not left to iter_csv_rows's own internal call. StreamingResponse sends the 200 status line before ever touching the body iterator, so a check inside an async generator raises too late for an HTTP client to see anything but a 200 with an empty body. This pre-flight is what usage.py's CSV export already does (checks run in the handler, not the generator); iter_csv_rows's own call stays in place as defense-in-depth for non-HTTP callers only. # List Audit Logs Source: https://developers.meshapi.ai/api/audit-logs/list-audit-logs /api/openapi.json get /v1/audit-logs The caller's org trail. `target.name` is the entity's CURRENT name, resolved at read time. It is not stored with the entry — a renamed entity therefore appears under its new name in older entries, and an entity that no longer exists has no name at all. # Get Balance Source: https://developers.meshapi.ai/api/balance/get-balance /api/openapi.json get /v1/balance Return the credit balance for the authenticated caller. Accepts either a dashboard session token (JWT) or an rsk_ API key. With an org context the org owner's balance is returned — all members of an org share the same billing pool; a caller with no org gets their own balance. The response also reports how much of the balance is reserved by in-flight operations (`reserved_usd`, plus a per-category `reserved_breakdown`) and the spendable remainder (`available_usd`). Auth: Authorization: Bearer # Cancel Batch Source: https://developers.meshapi.ai/api/batch/cancel-batch /api/openapi.json post /v1/batches/{batch_id}/cancel Cancel an in-progress batch. Partial results may still be available. # Create Batch Source: https://developers.meshapi.ai/api/batch/create-batch /api/openapi.json post /v1/batches Create a batch job. Accepts requests inline — no separate file upload step required. Model and provider are resolved from `body.model` across all requests; all requests must target the same model. Returns 429 (batch_limit_exceeded) if the owner already has 10 or more batches in a non-terminal state. Returns 501 (not_implemented) if the resolved provider doesn't support batch. # Get Batch Source: https://developers.meshapi.ai/api/batch/get-batch /api/openapi.json get /v1/batches/{batch_id} Get batch status from the upstream provider. Usage logging and billing fire automatically on the first terminal observation. When the batch is completed and has an output file, the response includes a `results` key with the parsed output (list of per-request response objects). # List Batches Source: https://developers.meshapi.ai/api/batch/list-batches /api/openapi.json get /v1/batches List batch jobs for the authenticated key's owner. Returns MeshAPI's own BatchJob records (unified view across all providers) in the OpenAI list format: `{"object": "list", "data": [...], "has_more": bool}`. Supports cursor-based pagination via `after` (batch_id) and `limit`. # Chat Compare Source: https://developers.meshapi.ai/api/chat/chat-compare /api/openapi.json post /v1/chat/compare Send one conversation to several models at once and get their answers together. Pass 2–10 model ids in `models` with the usual `messages`. They run in parallel and each result carries its own content, token usage and latency, so you can compare quality against cost on real traffic instead of guessing. Set `comparison_model` to have a further model judge the answers, optionally steered by `comparison_instructions`. Use `model_overrides` when one model needs different parameters from the rest; `temperature` and `max_tokens` otherwise apply to all. With `stream` true, results arrive as they finish rather than after the slowest. **Every model in the list is a billed call**, so a comparison across five models costs five completions plus the judge, and one model failing does not fail the request — that result reports its own error. # Chat Completions Source: https://developers.meshapi.ai/api/chat/chat-completions /api/openapi.json post /v1/chat/completions OpenAI-compatible chat completions endpoint. Auth: Authorization: Bearer rsk_ Streaming: set stream=true for SSE chunks Templates: set template="name" + variables={...} Rate limits: RPM and RPD enforced per key via Redis fixed-window counters Spend cap: enforced if key.spend_cap_usd is set (soft cap) # Create Embeddings Source: https://developers.meshapi.ai/api/embeddings/create-embeddings /api/openapi.json post /v1/embeddings Turn text into embedding vectors. OpenAI-compatible: point the OpenAI SDK's embeddings client at this gateway and it works unchanged. `input` takes a single string or a batch, and the response keeps the input order so you can zip results back to your records. `model` is required and must name a specific embedding model — `auto` is rejected, because a vector is only comparable to other vectors from the same model, so silently choosing one for you would corrupt an index. Provider failover and BYOK keys apply as they do to chat. # Erase an end user's identifier Source: https://developers.meshapi.ai/api/end-users/erase-an-end-users-identifier /api/openapi.json post /v1/end-users/erasures Removes a caller-supplied `user` identifier from the usage records it was attributed to, so a deletion request from your own end user can be passed through. Billing rows are kept and only the identifier is cleared, which is why this reports rows CLEARED rather than deleted. Idempotent: erasing an identifier that is already gone succeeds and reports `usage_events_cleared: 0`. **This is a point in time, not a lock.** Usage rows are written asynchronously after a request completes, so a request already in flight for this end user can re-attribute a row after the call returns. Erase once traffic for the identifier has stopped, or call again. # Get Error Catalog Source: https://developers.meshapi.ai/api/errors/get-error-catalog /api/openapi.json get /v1/errors Return the full error action-item catalog for client-side error surfacing. Resolution a client should apply: look up ``codes`` by its specific error identifier, fall back to ``categories`` by the error's category, then ``default``. The payload is small and slow-changing — cache it and re-fetch when ``version`` changes. # Make a structured decision Source: https://developers.meshapi.ai/api/evaluate/make-a-structured-decision /api/openapi.json post /v1/evaluate Get a typed answer with a calibrated probability, instead of prose to parse. Send your application `state` plus named `questions`. Each question declares its own `type`: `choice` picks one of your options and reports a probability for each, `score` rates against an ordered scale, and `noul` returns a single probability. `noul` is a **probability in [0, 1]**, not a true/false — despite the name. The answers come back keyed by your own question names. This endpoint is **beta**: it rides an upstream path we do not control, so the contract may change. # Edit an image Source: https://developers.meshapi.ai/api/images/edit-an-image /api/openapi.json post /v1/images/edits OpenAI-compatible image editing endpoint. Accepts EITHER ``multipart/form-data`` (binary uploads: ``image``, optional ``mask``, ``reference_images``) OR ``application/json`` whose image fields are data-URL / base64 strings (or ``{"url": ...}`` objects) — the same convention as the chat ``image_url``. Remote http(s) URLs are rejected (mesh does not server-side-fetch images); send a ``data:image/;base64,`` URL. Supports ``operation`` in {edit, remove_background, upscale, outpaint, mix, reframe, inpaint}. Each op is gated by the resolved model's supports_image_ flag (422 when unset / unsupported); the legacy provider matrix is a transitional backstop for the original four ops. - ``edit`` (default): openai, byteplus, vertex — prompt + image (+ optional mask + refs). - ``remove_background``: openai, vertex — image only; prompt optional. - ``upscale``: vertex (Imagen) or byteplus (Seedream Smart Upscale) — image + upscale_factor (x2|x4; Seedream maps x2→2k, x4→4k). - ``outpaint``: vertex — image + prompt + mask (defines expansion region). - ``mix``: Gemini image models + gpt-image (openai) — prompt + image + 1-3 reference_images (2-4 sources composed into one, sent as image[]); single output. - ``reframe``: Gemini image models — image + aspect_ratio (1:1|3:4|4:3|9:16|16:9|2:3|3:2); recomposes by outpainting to the target ratio; single output. - ``inpaint``: openai, vertex — image + mask (canonical WHITE=edit; mesh normalizes per provider). Optional mask_feather blends the result back through the mask (Pillow; 501 if unavailable). > **Note:** The image response may not render in GitBook's "Test it" panel. > To view the edited image, try this endpoint with Postman, curl, or another > HTTP client. # Generate Image Source: https://developers.meshapi.ai/api/images/generate-image /api/openapi.json post /v1/images/generations OpenAI-compatible image generation endpoint. > **Note:** The image response may not render in GitBook's "Test it" panel. > To view the generated image, try this endpoint with Postman, curl, or > another HTTP client. # Add Memory Item Source: https://developers.meshapi.ai/api/memory/add-memory-item /api/openapi.json post /v1/memories/{slug_or_id}/items Add an item (preference/guardrail/fact) to one of the caller's memories. # Create Memory Source: https://developers.meshapi.ai/api/memory/create-memory /api/openapi.json post /v1/memories Create a memory scoped to the authenticated caller's owner. # Delete Memory Source: https://developers.meshapi.ai/api/memory/delete-memory /api/openapi.json delete /v1/memories/{slug_or_id} Hard delete a memory (cascades to its items). Owner-scoped. # Delete Memory Item Source: https://developers.meshapi.ai/api/memory/delete-memory-item /api/openapi.json delete /v1/memories/{slug_or_id}/items/{item_id} Delete a single item from one of the caller's memories. Owner-scoped. # Get Memory Source: https://developers.meshapi.ai/api/memory/get-memory /api/openapi.json get /v1/memories/{slug_or_id} Get a single memory and its items. Must belong to the caller's owner. # List Memories Source: https://developers.meshapi.ai/api/memory/list-memories /api/openapi.json get /v1/memories List memories in the caller's org (MESH-467). Org owners/admins see every memory in the org — they are the ones who have to answer a deletion request. Plain members see only their own. # List Memory Items Source: https://developers.meshapi.ai/api/memory/list-memory-items /api/openapi.json get /v1/memories/{slug_or_id}/items List items for one of the caller's memories, ordered by position. # Memory Counts Source: https://developers.meshapi.ai/api/memory/memory-counts /api/openapi.json get /v1/memories/counts Totals for the dashboard's type rail (MESH-467). Scope MIRRORS ``list_memories`` — owners and admins see the whole org, plain members only their own, and legacy ``org_id IS NULL`` rows stay visible to their owner. It has to: a rail that counts rows the list then refuses to show is worse than no rail, and the shared ``_visible_memory_scope`` is what keeps the two from drifting apart. Deliberately unpaginated and unfiltered by search. These are the totals the nav shows, so they describe everything the caller can see rather than whatever the current page or query happens to contain — which is the whole reason the dashboard stopped summing the (capped) list client-side. # Memory Usage Source: https://developers.meshapi.ai/api/memory/memory-usage /api/openapi.json get /v1/memories/usage Per-memID request counts, injected tokens, and last-used, plus embed cost. MUST stay declared ABOVE `GET /{slug_or_id}` — FastAPI matches in declaration order, so below it "usage" is read as a slug and this route is unreachable (404, from a memory nobody owns). Scoped to the CALLER'S OWN keys, not the whole org, and that is the honest scope rather than a shortcut: injection is owner-scoped in the gateway, so a colleague's memID cannot be attached by your keys at all. It also sidesteps a real ambiguity — `memory_slugs` records slugs, and slugs are unique per (owner, slug) rather than per org, so an org-wide aggregate would silently merge two members' identically-named memories into one row. Every token figure is OUR tokenizer's estimate of what the injected block cost, not a provider-reported number: no upstream reports the split between the memory block and the rest of the prompt. A request attaching several memIDs logs ONE block size, which is split equally across them here — see the subquery. Hence `injected_tokens_est`, and hence no derived dollar figure beside it — see MemoryUsageRow. # Revive Expired Items Source: https://developers.meshapi.ai/api/memory/revive-expired-items /api/openapi.json post /v1/memories/{slug_or_id}/items/revive-expired Give every lapsed preference and fact in this memory a fresh deadline. Retention is enforced on READ (see ``resolver`` — it filters on ``expires_at``), so a lapsed item is already invisible to the gateway while its row survives until the sweeper hard-deletes it. This is how it comes back, and it is memID-WIDE on purpose: a partial revival would leave someone believing a rule or fact was restored when only some of them were. ``now() + retention_days``, NOT the ``created_at``-based re-stamping ``update_memory`` does. That difference is the entire point: re-dating a 41-day-old fact under a 30-day policy from its creation puts the new deadline 11 days in the PAST, so it would expire again the instant it was written and the button would look broken while doing exactly what it said. Retention itself is untouched — the revived items get a fresh window under the SAME policy and can lapse again. Reviving must not silently rewrite a retention policy someone chose. Guardrails are excluded, as everywhere else: they never carry a deadline, so there is nothing of theirs to revive. Mutation, so ``admin_ok=False`` — an org admin's extra powers are read and delete, not authoring. No vector work. Fact search resolves its hits against the rows this UPDATE touches, so an eligible row is retrievable again with nothing to re-embed. # Update Memory Source: https://developers.meshapi.ai/api/memory/update-memory /api/openapi.json patch /v1/memories/{slug_or_id} Update a memory's name, description, is_active, or retention. Owner-only. # Update Memory Item Source: https://developers.meshapi.ai/api/memory/update-memory-item /api/openapi.json patch /v1/memories/{slug_or_id}/items/{item_id} Edit one item's content, key, or position. Owner-only. ``item_type`` is NOT editable. It decides two things at once — which section of the composed block the item lands in, and whether the row is mirrored into Qdrant — so switching it is a delete plus a create, not an edit. In place it would also strand a preference's ``key`` on a fact, where nothing reads it. Callers who need a different type should delete and re-add. # Create Message Source: https://developers.meshapi.ai/api/messages/create-message /api/openapi.json post /v1/messages Anthropic-compatible Messages endpoint. Auth: x-api-key: rsk_ (or Authorization: Bearer rsk_) Models: any model this gateway serves, not only Anthropic's Streaming: set stream=true for Anthropic SSE events # Get Model Source: https://developers.meshapi.ai/api/models/get-model /api/openapi.json get /v1/models/{model_id} Return one enabled model by its model ID, with your discounted pricing. Served from the same cache as `GET /v1/models`. Discounts are applied per caller and never cached. # List a model's providers and their prices Source: https://developers.meshapi.ai/api/models/list-a-models-providers-and-their-prices /api/openapi.json get /v1/models/{model_id}/providers Every upstream provider that can serve this model, with that provider's own price and capabilities. `GET /v1/models` and `GET /v1/models/{model_id}` publish only the default price row — what a bare `model` string bills at. A model routed through several providers has a different rate on each, and resilient routing can fall back between them, so this endpoint lists them all. Which of them is the default is not published, by neither field nor position: it is a routing decision we re-make freely, and naming it would invite callers to depend on a specific upstream. Rows come back alphabetically. Discounts are resolved per row: the caller's model-scoped, provider-scoped and account-wide discounts all apply here, and the largest wins — so a discount attached to one provider shows up only on that provider's row. # List Models Source: https://developers.meshapi.ai/api/models/list-models /api/openapi.json get /v1/models List available models, with your discounted pricing when a discount applies. Only enabled models are returned. Accepts either a dashboard JWT or an API key. `is_free` marks a free model. Responses are cached for 5 minutes, and a catalogue change invalidates the cache immediately. # Search Models Source: https://developers.meshapi.ai/api/models/search-models /api/openapi.json get /v1/models/search Paginated, filterable, fuzzy-searchable model listing. Every multi-value filter is an OR within its dimension and an AND across dimensions. `facets` counts each dimension with its own filter dropped, so adding a second value to a dimension always widens the result set. There is intentionally no upstream-provider filter here. Which provider serves a model is not something a caller can pin on the inference API, so filtering the catalogue by it would offer a choice that doesn't exist. The providers are not secret — `GET /v1/models/{model_id}/providers` lists them with their prices — they just aren't a dimension of this listing. # Classify content against the content policy Source: https://developers.meshapi.ai/api/moderations/classify-content-against-the-content-policy /api/openapi.json post /v1/moderations Check text or images against the content policy before you act on them. OpenAI-compatible: `input` accepts a string, a batch of strings, or multimodal parts with image URLs. Each result reports `flagged` plus per-category booleans and scores, so you can set your own threshold rather than only trusting the flag. Images must be PNG, JPEG, GIF or WebP; anything else is rejected up front rather than failing upstream. Moderation is deliberately **not** gated by per-model allow-lists or usage caps — it is a safety primitive, and making it refusable would push callers toward skipping the check. Your key's rate limits still apply. # Create Thread Source: https://developers.meshapi.ai/api/playground/create-thread /api/openapi.json post /v1/playground/threads Create a playground thread owned by the authenticated caller. # Delete Thread Source: https://developers.meshapi.ai/api/playground/delete-thread /api/openapi.json delete /v1/playground/threads/{thread_id} Hard delete. A thread is scratch content — there is nothing to restore it for. # Get Thread Source: https://developers.meshapi.ai/api/playground/get-thread /api/openapi.json get /v1/playground/threads/{thread_id} Fetch one thread with its full message list. # List Threads Source: https://developers.meshapi.ai/api/playground/list-threads /api/openapi.json get /v1/playground/threads The caller's threads, most recently updated first. Returns summaries with a message COUNT rather than the messages themselves. A thread can hold half a megabyte of conversation, and the sidebar that consumes this only needs a label and a timestamp — shipping every turn of every thread to render a list would make the page slower the more the customer used it. Scoped by owner AND org. Owner alone is not enough: one person can belong to several orgs, and `owner` does not change when they switch. Filtering on it alone would show the threads they wrote for another customer's org in this one's playground. # Update Thread Source: https://developers.meshapi.ai/api/playground/update-thread /api/openapi.json patch /v1/playground/threads/{thread_id} Update a thread. This is the autosave path — called once per turn. # List providers an API key can be pinned to Source: https://developers.meshapi.ai/api/providers/list-providers-an-api-key-can-be-pinned-to /api/openapi.json get /v1/providers/pinnable Every provider this deployment can pin a key to, by display name. Store one as an API key's `fixed_provider` to make it the only provider allowed to serve that key. See the provider pinning guide for what the pin does to fallback and to billing. A provider that serves a surface but prices no listed model is left out: pinning to it would refuse every request a caller could discover how to make. That makes this list a strict subset of what `fixed_provider` accepts, never a superset, so anything offered here is valid. # List the models one provider serves Source: https://developers.meshapi.ai/api/providers/list-the-models-one-provider-serves /api/openapi.json get /v1/providers/{provider}/models The models a key pinned to `provider` can still call. Scoped to providers that can be pinned, because that is the question this answers — a slug outside that set is a 404 rather than an empty list, which would read as "pinned and serves nothing". # Delete File Source: https://developers.meshapi.ai/api/rag/delete-file /api/openapi.json delete /v1/files/{file_id} Delete a file: purge its vectors, delete the object, soft-delete the row. Ordering is load-bearing and fail-closed on the first step: 1. Qdrant points — retrieval filters on the Qdrant payload, NOT this table, so vectors that outlive the row would keep serving the document's content in search. A failure here aborts with 5xx and the row stays live, so the client can retry; the alternative (mark deleted, leave vectors) is a delete that silently didn't delete. 2. GCS object — best-effort by design (delete_gcs_object never raises). The signed download URL is already unreachable once the row is gone, and a stranded blob is a storage cost, not a disclosure. 3. The row — soft-deleted so usage_events.file_id stays resolvable for billing history. Every step is idempotent and the whole handler is safely retryable, which is what makes a failure recoverable rather than terminal. The final purge runs AFTER deleted_at is committed, so if Qdrant is down at that moment the row is already hidden — a retry that could not see it would 404 forever and leave any vectors an in-flight worker wrote stranded and searchable. So the lookup here deliberately includes soft-deleted rows: a retry re-runs the purge and converges. An unknown file_id is still a 404; an already-deleted one is 204. # Get File Status Source: https://developers.meshapi.ai/api/rag/get-file-status /api/openapi.json get /v1/files/{file_id} Get one file's processing status, and a short-lived download URL for it. Poll this after uploading the bytes: a file is only searchable once embedding has finished. A failure reports why here rather than by disappearing from the list. A file that is not yours answers **404**, not 403 — the response deliberately does not confirm that someone else's `file_id` exists. # Init Upload Source: https://developers.meshapi.ai/api/rag/init-upload /api/openapi.json post /v1/files Start a file upload and get a signed URL to PUT the bytes to. This call does not carry the file itself. It returns a `signed_url`, the `required_headers` you must send with it, an `expires_at`, and the `max_size_bytes` ceiling. Upload the bytes directly to that URL, then poll `GET /v1/files/{file_id}` for the status. With `embed` true (the default) the file is chunked and embedded once the bytes land, which makes it searchable through `POST /v1/files/search`. Any `metadata` you pass is stored per chunk and can be filtered on at search time. Accepted types: any `text/*`, PDF, DOC/DOCX, XLS/XLSX, PPTX, JSON, XML, SQL. Anything else is a 422 here, before you spend bandwidth on the upload. So is a `file_name` that is empty, has a `.` or `..` path segment, contains a control character, or is over 987 bytes. `text/*` is taken at face value: a text subtype with no dedicated parser — `text/rtf` is the one to watch — is chunked exactly as sent, so its markup ends up in the embeddings alongside the prose and dilutes search results. Convert such files to plain text or PDF before uploading. # List Files Source: https://developers.meshapi.ai/api/rag/list-files /api/openapi.json get /v1/files List the files you have uploaded, newest first. Scoped to the authenticated caller and the org they are acting in — you only ever see your own files, and only those uploaded in the current org. Each entry carries the same fields as `GET /v1/files/{file_id}`, including the embedding status, so you can poll this instead of one file at a time. Paginate with `limit` (1–200, default 50) and `offset`; `total` is the unpaginated count. # Trigger Embedding Bulk Source: https://developers.meshapi.ai/api/rag/trigger-embedding-bulk /api/openapi.json post /v1/files/embed Manually enqueue embedding jobs for one or more files. Each file must have upload_status=ready and embedding_status=pending or failed, or be queued/processing with no update for 5 hours. Returns a per-file result — successes are 'queued', failures include an error message. # Vector Search Source: https://developers.meshapi.ai/api/rag/vector-search /api/openapi.json post /v1/files/search Search your uploaded files and get back the matching passages. Hybrid retrieval: dense (embedding) and sparse (keyword) matches are fused with reciprocal rank fusion, so exact terms and paraphrases both work. Results are chunks, each with its `file_id`, score and stored metadata — ready to drop into a prompt as context. Narrow the search with `file_ids`, a `filter` on the metadata you supplied at upload, or a `date_from` / `date_to` window. `top_k` defaults to 5 (max 50). Only files uploaded with `embed` are searchable. **Each call embeds your query, so it is a billed request** — it counts against your rate limits and spend caps like any inference call. # Create Response Source: https://developers.meshapi.ai/api/responses/create-response /api/openapi.json post /v1/responses Responses API endpoint — provider resolved dynamically from DB. Auth: Authorization: Bearer rsk_ Streaming: set stream=true for SSE chunks Rate limits: RPM and RPD enforced per key via Redis fixed-window counters Spend cap: enforced if key.spend_cap_usd is set Provider: resolved from model_pricing.provider (same as chat/completions) # Router Select Source: https://developers.meshapi.ai/api/router/router-select /api/openapi.json post /v1/router/select Resolve the Auto Router's model choice for a prompt without running inference. # Create Template Source: https://developers.meshapi.ai/api/templates/create-template /api/openapi.json post /v1/templates Create a template scoped to the authenticated user's owner. MESH-1003: creates version 1 from the submitted content in the same transaction and auto-publishes it — a template is usable the instant it's created, same as before versioning existed. # Delete Template Source: https://developers.meshapi.ai/api/templates/delete-template /api/openapi.json delete /v1/templates/{template_id} Hard delete. Owner can delete own; org admin/owner can delete any org template. CASCADE takes every template_versions row with it — there is no separate "delete history" step. # Disable Template Source: https://developers.meshapi.ai/api/templates/disable-template /api/openapi.json delete /v1/templates/catalog/{template_id}/enable Remove a global template from the org's library. Idempotent. Removes it from the dashboard only — any key already calling this template by name keeps working (see the module docstring). # Enable Template Source: https://developers.meshapi.ai/api/templates/enable-template /api/openapi.json post /v1/templates/catalog/{template_id}/enable Add a global template to the org's library. Idempotent. # Get Template Source: https://developers.meshapi.ai/api/templates/get-template /api/openapi.json get /v1/templates/{template_id} Get a single template by UUID. Must belong to the caller's owner or their org. # Get Version Source: https://developers.meshapi.ai/api/templates/get-version /api/openapi.json get /v1/templates/{template_id}/versions/{version_id} # List Catalog Source: https://developers.meshapi.ai/api/templates/list-catalog /api/openapi.json get /v1/templates/catalog Browse global templates, with this org's enable state on each row. # List Categories Source: https://developers.meshapi.ai/api/templates/list-categories /api/openapi.json get /v1/templates/catalog/categories Category counts for the filter chips, plus the total behind the "All" chip. # List Templates Source: https://developers.meshapi.ai/api/templates/list-templates /api/openapi.json get /v1/templates List templates for the caller's org. Admin/owner can filter by member_owner. `scope` splits the two things this endpoint has always returned together: all org templates + every global (the historical response — DEFAULT, so the SDK and the MCP tools see no change) custom org templates only global only the globals this org has ENABLED `global` is the one that reads `template_enablements`, and it is confined to this endpoint: `resolve_template` at inference still resolves any global by name for any key, enabled or not. # List Versions Source: https://developers.meshapi.ai/api/templates/list-versions /api/openapi.json get /v1/templates/{template_id}/versions Newest first. A READ-only caller sees the published version only — an unpublished save is a draft, visible only to whoever could also edit it. # Publish Template Source: https://developers.meshapi.ai/api/templates/publish-template /api/openapi.json post /v1/templates/{template_id}/publish Publish `latest_version_id` — the one action that changes what inference resolves to. Idempotent: publishing the currently-published version is a no-op. # Rollback Version Source: https://developers.meshapi.ai/api/templates/rollback-version /api/openapi.json post /v1/templates/{template_id}/versions/{version_id}/rollback Copy `version_id`'s content into a new version and publish it immediately. Nothing is deleted — the version being rolled back FROM stays in history, and the new row records `reverted_from_version_id` for the audit trail. # Save Version Source: https://developers.meshapi.ai/api/templates/save-version /api/openapi.json post /v1/templates/{template_id}/versions Save a new immutable version. Never touches what's published — see POST .../publish. Any field left unset carries over from the current latest_version_id, matching the old content-bearing PATCH's ergonomics. # Update Template Source: https://developers.meshapi.ai/api/templates/update-template /api/openapi.json patch /v1/templates/{template_id} Rename/re-describe a template. Content is versioned separately — see POST .../versions. Owner can edit own; org admin/owner can edit any org template. # Get Rate Limits Source: https://developers.meshapi.ai/api/usage/get-rate-limits /api/openapi.json get /v1/usage/rate-limits Return live rate-limit usage (RPM / RPD / TPM) vs limits. Always reads fresh Redis counters — no caching. Accepts either a dashboard session token (JWT) — returning org/team/member scopes — or an rsk_ API key, returning a single `key` scope for that key. # Get Request Source: https://developers.meshapi.ai/api/usage/get-request /api/openapi.json get /v1/usage/requests/{request_id} Look up one request by the id returned in its `X-Request-Id` response header. Reports what the gateway knows about that request: the model and provider that served it, status, latency, tokens, cost, and every attempt made — so a retry or a fallback to another target is visible rather than inferred. Requests refused before any model was called (rate limit, spend cap, empty balance, model access) write no usage row, and are served from the request log with `outcome: "rejected"`. Two surfaces are not resolvable here, because their usage is not recorded against the HTTP request's id: `/v1/chat/compare`, whose fan-out is recorded per model under a derived id, and `/v1/batches`, whose usage is recorded against the batch id — use `GET /v1/batches/{batch_id}` for those. Resolvable for **90 days**. Older ids return 404, as does an id belonging to another account — existence is never confirmed across tenants. `request_headers` carries the HTTP headers of the request — what the gateway received, and what it returned (the rate-limit state we reported, the request id, the routing disclosure; never the provider's own set). Credentials are replaced at capture: `Authorization`, `x-api-key`, `api-key`, `x-goog-api-key` and cookies report their NAME with a `[redacted]` value, so "you sent no Authorization header" stays distinguishable from "you sent one". The client-IP forwarding chain is not reported. NULL when the request log no longer holds the record — the rest of the payload answers regardless. `request_bodies` carries the prompt and completion themselves, and is NULL unless the key that made the request has opted in to exposing them — its own policy governs, not the caller's. It cannot be enabled for an organisation with zero data retention on, which is also why a ZDR org has none to show. Where PII redaction is enabled this is the REDACTED copy; the raw body is never stored. A body over the size cap reports `truncated: true` with its original size rather than being silently shortened. `video_task` carries the current status and result of the video generation task a `POST /v1/video/generations` request created — the stored response body only ever shows the submit's `queued`. Read from our records; it never polls the provider. NULL for any other request. No provider-side diagnostic is ever included. Error text is the same scrubbed message the original response carried. # Get Spend Trend Source: https://developers.meshapi.ai/api/usage/get-spend-trend /api/openapi.json get /v1/usage/spend-trend Return historical spend bucketed by the chosen granularity, plus a trailing 3-bucket moving average and the window's total spend. # Get Usage Event Attempts Source: https://developers.meshapi.ai/api/usage/get-usage-event-attempts /api/openapi.json get /v1/usage/events/{event_id}/attempts Per-attempt timeline for one request (the served row + any retried rows sharing its request_id), oldest first — powers the logs drawer's "Attempts" history so a caller can see every model that was tried. Same customer-facing scrubbing as the events list, with one caveat: each attempt carries `provider`, the upstream it was attempted against. Treat that field as unstable — whether the upstream is exposed at all is under review. Authorization mirrors the events list: an API-key caller sees only events made by that key; a JWT caller must belong to the event's org, and a non-privileged member is restricted to their own events. Anything else 404s (existence is not leaked). # Query Rejections Source: https://developers.meshapi.ai/api/usage/query-rejections /api/openapi.json post /v1/usage/rejections Requests refused before any model was called — rate limit, spend cap, balance, model access (MESH-679). These never reach usage logging, so they are absent from /v1/usage/events by construction: a refusal has no tokens, no cost and no provider, and belongs in the request log rather than the billing ledger. Served from the request log, which has recorded them all along. Same auth and scoping as /v1/usage/events — an rsk_ key sees only its own refusals; a session token sees the org's, narrowed to the caller's own keys unless they are an org or team admin. # Query Usage Events Source: https://developers.meshapi.ai/api/usage/query-usage-events /api/openapi.json post /v1/usage/events Paginated per-request event history for the authenticated caller. Accepts either a dashboard session token (JWT) or an rsk_ API key; with an API key results are scoped to that single key (body org_id / member filters are ignored). Results are cached for 1 minute per org/filter combination for JWT callers. Pass ?refresh=true to bypass, or filter by `request_id`, which is never served from cache (see below). # Query Usage Summary Source: https://developers.meshapi.ai/api/usage/query-usage-summary /api/openapi.json post /v1/usage Aggregate usage stats for the authenticated caller. Accepts either a dashboard session token (JWT) or an rsk_ API key. With a JWT the stats span every key the caller can see in the given org; with an API key they are scoped to that single key (the body's org_id / member filters are ignored). Results are cached for 1 minute per org/filter combination for JWT callers. Pass ?refresh=true to bypass. # Create Video Generation Source: https://developers.meshapi.ai/api/video/create-video-generation /api/openapi.json post /v1/video/generations Create a BytePlus Seedance video generation task. Returns immediately with ``{"id": ""}``. Poll ``GET /v1/video/generations/{id}`` until status is ``succeeded``, ``failed``, or ``expired``. Pass ``callback_url`` to receive a POST notification when the task status changes. We intercept the BytePlus callback, update our DB, then forward the full task payload to your URL. # Get Video Generation Source: https://developers.meshapi.ai/api/video/get-video-generation /api/openapi.json get /v1/video/generations/{task_id} Retrieve the status and result of a BytePlus video generation task. Returns from DB when the task is in a terminal state. Otherwise polls BytePlus live and syncs the DB row. Poll until ``status`` is one of: ``succeeded`` — ``content.video_url`` is populated, ``failed`` — check ``error`` field, ``expired`` — task timed out. **Version-gated behaviour** - `video.poll_by_request_id` — poll a video task by its MeshAPI `request_id` as well as the provider task id. Requires `X-Mesh-Version: 2026-09` or later. # List Video Generations Source: https://developers.meshapi.ai/api/video/list-video-generations /api/openapi.json get /v1/video/generations List BytePlus video generation tasks persisted in MeshAPI's DB. This endpoint is intentionally DB-only: it does not poll BytePlus or update task state. Use ``GET /v1/video/generations/{id}`` to refresh a non-terminal task from the upstream provider. # Web Search Source: https://developers.meshapi.ai/api/web-search/web-search /api/openapi.json post /v1/web/search Search the live web and get back ranked results with snippets. Several engines sit behind this: the native engine is tried first, and on a failure or an empty result the request falls over to the next engine in the chain, so a single engine outage does not fail your request. Pin one with `provider` when you need reproducibility — pinning disables that failover. Shape results with `max_results` (1–20), `include_domains` / `exclude_domains`, and `search_depth` (`advanced` costs more and only affects Tavily; the other engines ignore it). The response says which engine answered. `model` exists only to name the native engine explicitly. It is not a model selector — the native engine has no model concept — and an unrecognised value is rejected with a 422 rather than silently ignored. # Changelog Source: https://developers.meshapi.ai/changelog New endpoints, breaking changes, deprecations, and SDK releases. Subscribe at [`/changelog/rss.xml`](/changelog/rss.xml) to hear about deprecations and breaking changes without checking back. Every entry that removes or changes existing behaviour is tagged **Deprecated** or **Changed**; where the change is a response shape, the entry says which API version carries it — see [API versioning](/docs/reference/api-versioning) for how to pin a version so a shape change cannot reach your code unannounced. ## Web search is priced per engine **Changed.** `POST /v1/web/search` is billed at the price of the engine that answered, instead of a flat \$0.005: `native` \$0.005, `tavily` \$0.008 (\$0.016 with `search_depth: "advanced"`), `tinyfish` \$0.005 plus \$0.001 per returned result carrying `page_content`. Failed and empty searches are still free. See [Web Search billing](/docs/capabilities/web-search#billing). ## Fixed `usage` keys on Responses and Messages; provider keys return on chat **Changed.** `POST /v1/responses` and `POST /v1/messages` now return a fixed set of `usage` keys whichever provider serves the request, all present and `0` when unused: * **Responses:** `input_tokens_details` (`cached_tokens`, `cache_write_tokens`, `cache_write_1h_tokens`, `audio_tokens`), `output_tokens_details.reasoning_tokens`, `total_tokens`, `byok_used` and `fallback_triggered`. * **Messages:** `cache_creation_input_tokens`, `cache_read_input_tokens`, `cache_creation` split by TTL, `byok_used` and `fallback_triggered`, on the response body and on `message_delta`. They were previously returned only when non-zero. On all three endpoints, keys a provider adds on its own (such as `is_byok`, `prompt_cache_hit_tokens` or `num_sources_used`) are returned alongside the fixed keys. On chat completions they had been removed since the change below. This applies to every API version. See [Usage Object](/docs/reference/usage-object). ## One `usage` shape on chat completions **Changed.** `usage` on `POST /v1/chat/completions` (including the final chunk of a stream) now has the same keys whichever provider serves the request, all present and `0` when unused: `prompt_tokens_details` (`cached_tokens`, `cache_write_tokens`, `cache_write_1h_tokens`, `audio_tokens`), `completion_tokens_details`, `byok_used` and `fallback_triggered`. `total_tokens` is always `prompt_tokens + completion_tokens`. Keys a provider added on its own, such as `is_byok` or `prompt_cache_hit_tokens`, are no longer returned; a provider's cache hits appear as `cached_tokens`. This applies to every API version. See [Reading the result](/docs/capabilities/caching#reading-the-result). **Deprecated.** The top-level `usage.cache_creation_input_tokens` on Claude responses duplicates `prompt_tokens_details.cache_write_tokens`. It is still returned; retirement date to be announced. ## Limit a BYOK key to specific models A provider key you register with Mesh used to serve every model Mesh routes to that provider. **Allowed models** on the key (`allowed_models` in the API) now limits it to exact model IDs or brand wildcards such as `openai/*`. A model outside the list is served on Mesh's credentials and billed to your balance, or, with fallback off, refused with `403 byok_model_not_allowed`. See [Limit a key to specific models](/docs/capabilities/byok#limit-a-key-to-specific-models). ## Mesh Code Mesh Code is a desktop app for agentic coding, with every Mesh model behind one key. It plans, writes files, runs commands and starts dev servers inside a project folder you choose; each step is either auto-approved by a mode you pick or shown to you as a diff first. Version 0.2.4 (1 October) ships for macOS (Apple silicon), Linux (AppImage) and Windows (x64). Download it from [GitHub](https://github.com/aifiesta/mesh-code). ## fal image models on `/v1/images/edits` `fal-ai/gemini-3-pro-image-preview/edit` and `fal-ai/nano-banana-pro/edit` serve `POST /v1/images/edits`, with one source image or several. They bill at the model's per-image or per-megapixel rate. See [Edit an image](/docs/capabilities/image-generation#edit-an-image). ## Clearer audio errors * Gemini speech-to-text accepts `.m4a`, `.mp4`, `.webm` and `.opus` audio, and a file a model cannot take is refused with a message that says why, not a generic capability error. * A rate-limited ElevenLabs speech-to-text request moves on to fallback instead of failing as `422` `model_id: Field required`. ## Fixes * A `tool` message whose `content` is a list of parts reaches Vertex Gemini models as its text instead of failing with `500`. * A multipart upload sent to a JSON endpoint is a `422` validation error, not a `500`. * Large usage exports no longer fail partway through. ## Poll a video task with the id you already have Version `2026-09` is out (released 3 September). Pin it and `GET /v1/video/generations/{id}` accepts the `request_id` Mesh returned when you created the task, as well as the provider task id: ```bash theme={null} curl "https://api.meshapi.ai/v1/video/generations/req_01ARZ3NDEKTSV4RRFFQ69G5FAV" \ -H "Authorization: Bearer rsk_..." \ -H "X-Mesh-Version: 2026-09" ``` Useful when the request id is what your logs and your database already hold, so you no longer have to store a second identifier just to check on a video. The task id is tried first and the request id only when it misses, so nothing you poll today changes. Both lookups are scoped to the key's owner: an id that belongs to someone else is a `404`, never a leak. Video task bodies also gained a `request_id` field — on the create response, on `GET /v1/video/generations/{id}`, and on every row of the `GET /v1/video/generations` list. Under `2026-08` the field is stripped, so a pinned parser sees exactly what it saw before. Two things worth knowing before you switch: * Tasks created before 20 August 2026 predate the recorded request id and stay reachable by task id only. * `2026-08` is still the baseline, so an unpinned request is unaffected. Send the header (or [pin it on your key](/docs/reference/api-versioning#pinning-a-whole-api-key)) to use this. `GET /v1/api-versions` now reports a `capabilities` list per version — `2026-09` carries `video.poll_by_request_id` — so you can check what a version gives you without reading the changelog. The list is cumulative: a later version includes everything an earlier one introduced. See [API versioning](/docs/reference/api-versioning). ## Model pricing shows the rate you are billed Under `2026-09`, when your account has a discount on a model, its `pricing` object on `GET /v1/models`, `GET /v1/models/search` and the per-model reads carries `discounted`: the same rates with the discount applied. That covers tokens, per-image, per-second video, resolution tiers, cache reads and `price_matrix` entries. With no discount the field is absent. List prices are unchanged. `tool_pricing` is not included, since a hosted-tool fee is charged after the discount. Under `2026-08` the field is stripped. Where a discount covers only some of the provider accounts that can serve a model, `discounted` shows the best rate attainable; a request bills at the rate of the account that served it. A request served on your own provider key (BYOK) is billed without the discount. ## Video billing follows what the provider renders **Changed.** These video routes now bill the clip the provider renders: * DeepInfra `google/veo-3.1` and `google/veo-3.1-fast` bill their fixed 8 s clip, not 5 s. * AtlasCloud video bills per second at the resolution tier. `alibaba/wan-2.7` is $0.10/s at 720P and $0.15/s at 1080P or with no resolution sent. Most other AtlasCloud video models are re-priced per second from AtlasCloud's own quotes, with a separate audio rate where AtlasCloud charges one. A request that leaves out duration, resolution or audio bills what AtlasCloud renders by default. * Runware video bills its sound rate whenever sound is rendered, including when the model adds it by default (`google/veo-3.1` does unless you turn it off). * Venice video bills Venice's quote for the duration, resolution and audio you ask for, not one flat price. * A BytePlus Seedance request with no `resolution` bills the tier it renders by default (720p for Seedance 2.x and 1.5 pro, 1080p for 1.0 pro). It used to bill a higher rate. Two requests are now refused at submit instead of billed at the wrong rate: sound on a Runware model that prices sound separately but has no audio rate for that clip, and an AtlasCloud per-second request no rate covers (Kling o3 and Omni `video-edit`, Grok `edit-video`, and the `-SR` super-resolution tiers). ## A video input a model cannot take is now a `422` Send a `content` item whose role the serving model does not accept — a closing frame to a model with no concept of one, a reference video to a model that takes only images — and the submit is now rejected with `422`. The message names the item by its position in `content` and, where the item was reinterpreted on the way in, what it resolved to. It used to be dropped: the generation ran without it and you were billed for a video built from inputs you did not send. For example, `bytedance/seedance-1.0-lite` takes an opening frame but not a closing one, so a second image sent as the `last_frame` is refused: ```json theme={null} { "error": { "code": "unprocessable_entity", "message": "Model 'bytedance/seedance-1.0-lite' on provider 'together' cannot use content[1] (image_url → last_frame). It would be dropped and the generation billed anyway — remove it, or choose a model that takes it." }, "request_id": "req_..." } ``` **Top-level parameters are unchanged.** A tunable the model does not implement is still accepted and reported in `unsupported_params` — a dropped parameter still gives you the video you asked for, tuned differently, while a dropped asset changes what was generated. **This applies on every API version.** It is a refusal, not a response shape, so pinning `X-Mesh-Version` does not hold the old behaviour. Check `supports_video_first_frame` and its five siblings on `GET /v1/models` before you send, and `GET /v1/models/{model_id}/providers` to see whether any route takes the input. Full detail: [Finding out what a model accepts](/docs/capabilities/video-generation#finding-out-what-a-model-accepts). ## Error messages name the provider that answered **Changed.** An error from an upstream provider now keeps that provider's name in `message`, `upstream_error` and `provider_error.provider`. On our own provider keys a cloud or reseller provider's name (OpenRouter, Bedrock, Vertex, BytePlus, …) used to read "the provider"; your own keys (BYOK) always showed it. Only the name is new: cloud resource ids and anything credential-shaped are still removed, and a problem with our own provider account still reads as a generic `upstream_error`. The name describes who answered that one request, not a routing guarantee. See [What errors never contain](/docs/reference/errors#what-errors-never-contain). ## Stop reasons arrive as the provider sent them **Changed.** `finish_reason` is mapped to an OpenAI value only where the meaning is the same (`end_turn` → `stop`, `content_filtered` → `content_filter`) and is forwarded verbatim otherwise, so a Gemini `MALFORMED_FUNCTION_CALL` or an Anthropic `pause_turn` no longer reads as `stop`. Every chat choice also carries `native_finish_reason`, the provider's untranslated value, or `null` when the provider sent none. This applies on every API version, streamed or not. ## Evaluate: typed answers instead of prose `POST /v1/evaluate` asks a model structured questions about a piece of application state and returns each answer typed: a chosen option with a probability for every option, a score on a scale you define, or a single probability. It is served by `typesafe/jev-1.13` and is in beta, so the contract may change. A key restricted to specific scopes needs `evaluate:write`. The same call is the `evaluate` tool on the [MCP server](/agent/mcp). See [Evaluate](/docs/capabilities/evaluate). ## Prompt templates keep a version history Template content is stored as immutable versions. Saving a version leaves the live one alone; publishing switches it: * `POST /v1/templates/{id}/versions` saves a version; `GET .../versions` and `GET .../versions/{version_id}` read them. * `POST /v1/templates/{id}/publish` makes the latest saved version live. * `POST /v1/templates/{id}/versions/{version_id}/rollback` republishes an earlier version's content as a new version. * A chat request can set `template_version` to run an exact version instead of the published one. **Deprecated:** the content fields on `PATCH /v1/templates/{id}`: `system`, `messages`, `model`, `params` and `variables`. They are still accepted, and a `PATCH` that carries them saves a new version and publishes it immediately. No retirement date is set. See [Prompt templates](/docs/capabilities/prompt-templates). ## Erase an end user's identifier `POST /v1/end-users/erasures` takes `{"end_user_id": "..."}`, the identifier you sent as `safety_identifier` or `user`, and clears it from your organization's usage records, so a deletion request from one of your own users can be passed through. Billing rows are kept; only the identifier is removed. Request logs are not erased, and `pending` always lists them. It also lists usage records when one call hits its row cap; call again to finish. The analytics copy is not erased either; it expires within 90 days. A request still in flight can re-attribute a row, so call again once traffic for that identifier has stopped; the call is idempotent. The key must belong to an organization; a key restricted to specific scopes needs `end-users:write`. ## `safety_identifier` and the `reasoning` object **Deprecated:** `user`, in favour of `safety_identifier`, the name OpenAI moved the field to. Both are accepted on chat completions, Responses and embeddings, and `safety_identifier` wins when both are sent. `safety_identifier` is also accepted on images, audio, video, batch, compare, moderations, web search, file search and `/v1/messages`. The dashboard's usage CSV export can be filtered to one end user by the exact `safety_identifier` sent. **Deprecated:** `reasoning_effort` on `/v1/chat/completions`, in favour of an OpenRouter-compatible `reasoning` object. Send `reasoning.effort` (`max`, `xhigh`, `high`, `medium`, `low`, `minimal` or `none`) or `reasoning.max_tokens`, a thinking budget, but not both. When the object and `reasoning_effort` are both sent, the object wins. OpenRouter receives the object as sent. Other providers get the effort it implies where they take one, with a `max_tokens` budget mapped to `low`, `medium` or `high`. `exclude: true` (reason without returning the reasoning) is honoured only on OpenRouter and on OpenAI models served through the Responses API. Neither deprecated field has a retirement date yet. ## Flex service tier `service_tier: "flex"` on `/v1/chat/completions` and `/v1/responses` serves an OpenAI model from OpenAI's slower, cheaper flex pool and bills it at OpenAI's flex rates. A model that cannot serve flex is refused rather than billed at standard rates. It launched on the OpenAI models with published flex prices, and `openai/gpt-6-sol` and `openai/gpt-6-luna` joined later in the month. See [Service Tiers](/docs/capabilities/service-tiers#which-models-serve-flex). **Changed.** `service_tier` takes only `auto`, `default` and `flex`. `priority`, `fast` and `scale` used to be accepted and dropped; they are now a `422`. ## Model discovery * `GET /v1/models/search` takes `capability=` (for example `tools`, `thinking`, `image_edit`), returns `facets` counting the results each filter value would give, and sorts with `sort=price` when paired with exactly one `output_modality` of `text`, `image` or `video`. * Discovery follows the calling key's model policy: `GET /v1/models`, `/v1/models/search` and the per-model reads list only models the key may call, and a per-model read of an excluded model is `403`. * **Changed.** `supports_batching`, on `/v1/models` and `/v1/models/{id}/providers` and in the `batching` filter, is `true` only where `/v1/batches` can run the model. A model whose default provider is Anthropic, Vertex or Bedrock now reads `false`; `/v1/batches` answered `501` for it. See [Models](/docs/reference/models#listing-available-models). ## Exclude models from a key, an organization or a team An API key can carry `blocked_models` beside `allowed_models`: exact model ids or brand wildcards it may not call. Exclusion wins over the allow-list, and the Auto Router, fallbacks and `/v1/router/select` respect it. A refused request is `403 model_excluded`, naming the model. Set it in the key drawers under **API Keys**, or on `/v1/keys` with an admin key. Model access can also be set for the organization, a team or a member in the dashboard; every layer has to permit a model. See [API keys](/docs/getting-started/api-keys#model-allow-lists). ## Pin a key to one provider `fixed_provider` on `POST` and `PATCH /v1/keys` makes one provider the only one that may serve that key. There is no cross-provider fallback, and a model that provider does not serve is `400 model_not_available_on_pinned_provider`. `GET /v1/providers/pinnable` lists the providers you can pin to, and `GET /v1/providers/{provider}/models` lists what a pin still serves. ## Key routing policy and rate-limit tiers * **Changed.** A saved key routing policy stores only the fields you set, so an omitted field follows the gateway default. A policy saved before this change keeps what it stored, which can include `fallback.enabled: false` on a key whose owner only turned on retries. See [Per-key routing policy](/docs/platform/retry-and-fallback#per-key-routing-policy). * Your organization's rate-limit tier sets how far its admins can raise a key's `rpm_limit` and `rpd_limit`. `standard` allows 1,000 rpm and 1,000,000 rpd, as before; `enterprise` allows 20,000 and 5,000,000. Mesh sets the tier; ask [support](/docs/reference/support) to change it. ## New webhook events * `payment.failed`: a Stripe or Cashfree top-up failed, or an auto-recharge charge failed (which also fires `auto_recharge.failed`). Your balance is unchanged. * `byok_key.failed`: one of your provider keys was rejected upstream; the payload says whether the request was served on Mesh credentials instead. * `provider.fallback`: a request was served by a different provider than the one requested. It is sent at most once every 5 minutes for each pair of providers and model. See [Notifications](/docs/capabilities/notifications#available-events). ## New models * Gemini audio: `google/gemini-3.5-transcribe` (speech-to-text) and `google/gemini-2.5-flash-tts` on Vertex; `google/gemini-3.1-flash-tts`, already on Venice, now also on Vertex as a fallback; `google/gemini-3.8-flash-tts` and `google/gemini-3.8-flash-lite-tts` on AI Studio. See [Text to Speech](/docs/capabilities/text-to-speech) and [Speech to Text](/docs/capabilities/speech-to-text). * ElevenLabs: `elevenlabs/eleven_v4` and `elevenlabs/eleven_v4_turbo` text-to-speech. * GPT Image 2.5: `openai/gpt-image-2.5-flare` and `openai/gpt-image-2.5-sunburst`, with edits, inpainting and reference images. See [Image Generation](/docs/capabilities/image-generation). * Sync Lipsync v3 on fal: `fal-ai/sync-lipsync/v3` lipsyncs a video to an audio track, and `fal-ai/sync-lipsync/v3/image-to-video` animates a still image. See [Video Generation](/docs/capabilities/video-generation). ## Pin the API contract to a date Mesh now versions its public contract **by date**, in a request header. The current version is `2026-08` (released 15 August): ```bash theme={null} curl https://api.meshapi.ai/v1/models \ -H "Authorization: Bearer rsk_..." \ -H "X-Mesh-Version: 2026-08" ``` A pinned request states which response shapes your code can parse, so a future change to those shapes cannot reach it. `/v1` stays exactly where it is — it is a namespace, not a version, and there is no `/v2` planned. **You do not have to do anything.** Sending no header gets you the *oldest* supported version, so publishing a new one never moves existing traffic. * `GET /v1/api-versions` lists every version served, with its `status`, release date, and which one is the `baseline` and which is `latest`. * An API key can carry a pin, so code you do not control still gets a fixed contract — set it on the key's **API Version** field under **API Keys**. A header on the request wins over the key's pin. * Every response echoes `X-Mesh-Version` with the version that served it. * A version Mesh does not serve is rejected with `400 invalid_api_version` rather than silently downgraded, so a typo'd pin fails immediately instead of quietly serving you something else. The error body lists `supported_versions`. When a version is eventually retired, its responses will carry standard `Deprecation` and `Sunset` headers first, so monitoring can notice before anyone reads a page. Nothing is deprecated today. Full detail: [API versioning](/docs/reference/api-versioning). ## Anthropic Messages API `POST /v1/messages` speaks Anthropic's Messages format, so a client built on Anthropic's SDK, [Claude Code](/docs/capabilities/claude-code) included, can point at Mesh unchanged and address any model Mesh serves, not only Claude. Streaming, tool use and vision are supported. See [Messages API](/docs/capabilities/messages-api). ## Playground saves conversations The dashboard's chat Playground saves each conversation as a thread, together with the request configuration that produced it, so reopening a thread restores the run. Threads are visible only to you. ## Node.js SDK 2.0.0 — the request id rides on what each call returns **Breaking:** `MeshAPIConfig.onResponse` and the `ResponseInfo` type are gone. The hook was configured once per client, so with several calls in flight it could not say which response belonged to which call. The id now lives on the value the call returned: ```typescript theme={null} const completion = await client.chat.completions.create(params); console.log(completion.requestId); const stream = client.chat.completions.create({ ...params, stream: true }); console.log(await stream.requestId); // resolves when headers arrive, before the first chunk for await (const chunk of stream) { /* ... */ } ``` * Every streaming method returns an `SSEStream`, an `AsyncIterable` that also carries `requestId` as a promise. It stays readable after a mid-stream failure or an abort, which is when you most want it. * `SSEStream.cancel()` releases the connection for a stream you read the id from but never iterated. It is also wired to `Symbol.asyncDispose` for `await using`. * Breaking out of a `for await` over a stream now closes the connection; previously it leaked the socket. * Iterating one stream object twice no longer issues, and bills, a second request. * Errors raised from a mid-stream error frame no longer report an empty `requestId`. Widening the return types is source-compatible: only code that set `onResponse` has to change. See [Streaming](/sdk/streaming) and [Error handling](/sdk/error-handling). ## SDK releases * **Node.js 2.0.0** — request id on every returned value; `onResponse` removed (above). * **Node.js 1.0.4** — repaired Responses API streaming and the realtime ESM entry point; clearer error messages; `models.free()` / `models.paid()` now filter server-side via `?free=`; the SDK version header matches the package version again. * **Node.js 0.1.2** — exposed `x-request-id` on successful responses via `onResponse` (superseded by 2.0.0 four days later). Tagged `v1.0.2` in the repository. ## Translate audio to English `POST /v1/audio/translations` accepts audio in any language and returns the speech translated to English. This is distinct from the transcribe-and-translate helper at `POST /v1/audio/transcriptions/translate`, which transcribes first and then translates the text. ```python theme={null} client.audio.audio_translate(audio_bytes, AudioTranslationsParams(model="..."), filename="...") ``` ```typescript theme={null} client.audio.translations(audioBytes, { model: "..." }, { filename: "..." }); ``` ```go theme={null} client.Audio.Translations(ctx, audioBytes, "filename.mp3", meshapi.AudioTranslationParams{Model: "..."}) ``` `model` is required — check `GET /v1/models` for models that support translation. A model that does not is refused with `422`. See [Audio](/docs/capabilities/audio). ## Structured outputs in the SDKs Node.js 0.1.1 adds a `parse()` method on chat completions that sends a JSON-schema `response_format` and returns a typed value instead of a string: ```typescript theme={null} const country = await client.chat.completions.parse(params, Country); ``` It takes any Standard Schema validator (Zod, Valibot, ArkType) or a raw schema. The Python `parse()` is not yet published to PyPI. See [Structured output](/sdk/structured-output). ## Realtime audio — GA protocol Python 0.1.11 and Go v0.1.12 move the realtime client to the GA wire protocol: audio is sent as base64 `input_audio_buffer.append` events, and output audio is decoded into the message's `audio` field rather than left as a base64 string. See [Realtime](/sdk/realtime). ## SDK releases * **Python 0.1.11** — realtime GA protocol. * **Python 0.1.10** — realtime TLS trusts the `certifi` bundle; stricter request params; error mapping matches the gateway's error codes; `image_bytes()` helper for reading generated images; spec fields that had been dropped are now declared. * **Python 0.1.9** — transcription `language_code` example fixed. * **Python 0.1.8** — audio translations. * **Go v0.1.12** — realtime GA protocol. * **Go v0.1.11** — SSE buffer no longer truncates large frames; error mapping matches the gateway's error codes; `Bytes()` helper for generated images; missing spec fields added. * **Go v0.1.10** — transcription `language_code` example fixed. * **Go v0.1.9** — audio translations. * **Node.js 0.1.1** — covers the full public data plane: moderations, web search, router select, models search and get, Responses API list and get, image edits, audio translations, plus structured outputs. The unreleased `/v1/documents` wrapper was removed. Versions are as published to PyPI (`meshapi`), npm (`meshapi-node-sdk`) and the Go module proxy. Before 1.0.4, Node.js repository tags do not match npm: npm 0.1.1 is tagged `v1.0.0` and `v1.0.1`, and 0.1.2 is `v1.0.2`. See [SDK overview](/sdk/overview) for install commands. # Auto Routing Source: https://developers.meshapi.ai/debug/auto-routing Why the auto router picked the model it did, and what to do when routing fails. Auto Routing (`model: "auto"`) classifies each request and picks a model for you. Most confusion is about *which* model ran, *what* you were billed, and *where* it's supported. See [Auto Routing](/docs/capabilities/auto-routing) for the full guide. ## 'auto' didn't route on my endpoint Auto Routing only works on a few inference endpoints: | Endpoint | Streaming | | - | - | | `POST /v1/chat/completions` | Yes | | `POST /v1/responses` | Yes | | `POST /v1/messages` | Yes | Setting `model: "auto"` on any other endpoint won't route. Use a concrete model ID there. `POST /v1/embeddings` rejects `"auto"` with `400 unprocessable_entity`. ## Which model actually served my request? Mesh injects the resolved model into the response, but where depends on streaming: * **Non-streaming**: in the body, as `x_resolved_model_id` and `x_auto_routed`. * **Streaming** (`stream: true`): as HTTP response headers sent before the SSE stream, `X-Auto-Routed` and `X-Resolved-Model-Id`. They are not in the body, which is why people miss them on streams. `POST /v1/messages` is the exception. A non-streaming Messages response echoes `"model": "auto"` and drops the `x_auto_routed` and `x_classifier_usage` fields, so only a streaming request reports the pick (in `X-Resolved-Model-Id`). For a non-streaming one, check Dashboard → **Logs**. ## It keeps picking a cheap/default model (e.g. qwen3.5-27b) The router never blocks a request on its own failure. Its algorithms run as a waterfall; if every one of them declines, times out or errors, it **falls back to a reliable default**, currently `qwen/qwen3.5-27b`. Note this is *not* a classifier model. The classifiers (for example `openai/gpt-4o-mini`) decide; the default is what serves the request when every algorithm missed. Landing on one is a very different signal from landing on the other. Check `x_classifier_usage` for the classifiers that ran. Check the response for: ```json theme={null} { "x_auto_routed_fallback": true, "x_auto_routed_fallback_reason": "all_strategies_failed" } ``` If you always land on the fallback, routing is failing, not your prompt. A reason of `entitlement_filtered`, `exclude_filtered` or `capability_filtered` means the router's pick wasn't usable for this request (your model policy, your `exclude_models`, or tools) and it chose another model. See [Fallback behavior](/docs/capabilities/auto-routing#fallback-behavior). Short, conversational prompts (greetings, one-line questions) can also go to a dedicated fast-lane model without any fallback. That's the `heuristic` algorithm, not a failure. ## Costs are higher than the resolved model alone With Auto Routing you're billed for **two** things: the tokens of the resolved model **plus** the tokens consumed by the internal classifier model. Both show up in the usage dashboard. For cost-sensitive or high-volume paths, pin a specific model instead. ## I need control over the model choice `auto` gives the gateway full discretion. If you need determinism: * Pin a single model ID, or * Supply a `models` fallback array to constrain the set of models a request may use, instead of `"auto"`. ## First-token latency went up The classification step adds an extra internal LLM call before your request is forwarded, which increases time-to-first-token. Two cases skip it: trivial conversational prompts answered by the fast lane, and a prompt classified in the last 24 hours, whose classification is cached. For latency-sensitive interactive use, pin a model rather than using `auto`. ## `model: "auto"` returned an error | Status | Code | Cause | | - | - | - | | `400` | — | The Auto Router is disabled on this gateway | | `400` | `unprocessable_entity` | You sent `"auto"` to `/v1/embeddings` | | `500` | `internal_error` | A server-side fault, such as the gateway's default model not being configured. Contact support with the request ID | ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # Batch API Source: https://developers.meshapi.ai/debug/batch Fix batch job submission errors, stuck jobs, and result download problems. The Batch API is asynchronous and built for throughput, not latency. See [Batch API](/docs/capabilities/batch-api) for the full guide. ## Submission errors | Status | Code | Cause | Fix | | - | - | - | - | | `400` | `model_not_supported_for_batching` | The model isn't batchable. Only models with `supports_batching: true` in `GET /v1/models` (today, OpenAI models) are | Pick a batchable model | | `400` | `mixed_models` | Requests target more than one model | See below | | `400` | `invalid_batch_file` | No request in `requests` has a `body.model` | Set `model` in each request's `body` | | `400` | `model_not_available_on_pinned_provider` | Your key is pinned to a provider that can't serve this model | Pick a model that provider serves, or remove the pin | | `403` | `model_excluded` | Your model policy doesn't permit this model | Ask an org admin to allow it, or pick a permitted model | | `404` | `model_not_found` | The model ID doesn't exist | Check `GET /v1/models` | | `402` | `spend_limit_exceeded` | Balance too low, or a spend cap reached | Top up, or raise the cap | | `429` | `batch_limit_exceeded` | You already have 10 active batches | See below | | `501` | `not_implemented` | The model's provider has no batch support | Pick a batchable model | | `404` | `batch_not_found` | (Get / cancel) No batch with that ID for this key's owner | Check the ID | ## 400 mixed\_models — all requests must use one model A batch **cannot mix models** — every item in `requests` must target the same model. Split different models into separate batches. ## 429 batch\_limit\_exceeded You can have at most **10 batches in a non-terminal state** at once. Wait for in-flight batches to reach a terminal status (`completed`, `failed`, `cancelled`, `expired`) before creating more. ## Results are out of order / I can't match them Output order is **not guaranteed**. Match each result to its request by the `custom_id` you supplied (it's echoed back on the matching result). ## Batch completed but some items failed A `completed` batch can still contain failed items. Check two levels: * **Batch level**: `status` and `request_counts` (`total` / `completed` / `failed`). * **Item level**: each entry in `results` has a `response.status_code`. Requests that failed are listed separately in the `errors_detail` array, matched by `custom_id`. `results` and `errors_detail` are only returned for `completed` batches. For a `failed` or `expired` batch, the reason is in the batch's `errors` object; a `cancelled` batch returns no results. ## It's taking too long / expired Batches are optimized for throughput, not interactive latency. They run within the `completion_window` you set (e.g. `"24h"`); if a batch doesn't finish in time it moves to `expired`. Don't use batches for low-latency calls — use `POST /v1/chat/completions` directly. ## Managing in-flight batches * `GET /v1/batches` lists recent batches. * `POST /v1/batches/{batch_id}/cancel` cancels one (moves through `cancelling` → `cancelled`). * Statuses progress `validating → in_progress → finalizing → completed`. ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # BYOK Source: https://developers.meshapi.ai/debug/byok Diagnose bring-your-own-key failures: credential shape, provider permissions, and silent fallback. Most BYOK failures are **configuration** problems: the credential is shaped wrong for the provider, the underlying account lacks a permission, or fallback is masking a broken key. See [BYOK](/docs/capabilities/byok) for the full setup reference. BYOK errors carry their own codes, so you can tell a provider-key failure from an `rsk_` key failure: | Provider said | You get | `error.code` (`provider_code`) | | - | - | - | | `401` / `403` | `401` | `upstream_auth_error` (`invalid_auth`, `permission_denied` or `account_not_entitled`) | | `429` | `429` | `upstream_rate_limit_error` (`rate_limit_exceeded`) | | Out of credit or monthly quota | `402` | `insufficient_credits` (`insufficient_credits` or `monthly_quota_exceeded`) | | `5xx` / unreachable | `503` | `upstream_error` | A `401 upstream_auth_error` means *your provider key* was rejected. A plain `401 unauthorized` is your `rsk_` key. ## 401 — provider key invalid or revoked The provider rejected the credential Mesh forwarded. * The key was rotated, revoked, or copied with a typo / trailing whitespace. * For **Bedrock API keys**, the key is tied to a **specific AWS region** — a key issued for one region won't work for a model served from another. * Re-enter the key in the dashboard and confirm it's active in the provider console. ## 401 permission\_denied — key is valid but lacks permission for the model Authentication worked, but the account isn't authorized for that model or action (`provider_code` `permission_denied` or `account_not_entitled`). * **AWS Bedrock** — the IAM user/role is missing the required actions. Attach `bedrock:InvokeModel`, `bedrock:InvokeModelWithResponseStream`, and `bedrock:ListFoundationModels`. * **Google Vertex AI** — the service account needs the **Vertex AI User** role (`roles/aiplatform.user`) or at least `aiplatform.endpoints.predict`. * The model may not be enabled/available in your provider account or region. ## Wrong credential shape (Bedrock / Vertex) Each provider expects a specific format: * **OpenAI** — just the API key string. * **Anthropic, Arcee, Cerebras, Cohere, DeepSeek, Fireworks, Flatkey, Moonshot, Parasail, Perplexity, Qwen, Sarvam**: the API key string only. * **AWS Bedrock**: either a Bedrock API key or IAM credentials. Registering either needs `credential_type`, `region` and `team_id`; `credential_type` must match the contents: `iam_credentials` (with `access_key_id`, `secret_access_key`) vs `bedrock_api_key`. * **Google Vertex AI** — the full service-account JSON. `region` is optional and defaults to `"global"`; set it if your model is region-specific. A mismatched `credential_type` or a missing `region` is a frequent cause of silent failures. ## 429 — rate limited on your own provider account This is **your provider's** rate limit, not Mesh's. Requests-per-minute and token limits are governed by your OpenAI / AWS / Google account. * Raise the quota in the provider console, or slow down. * With fallback enabled, Mesh retries on platform credentials — so you may see success *and* a hidden fallback (see below). ## Fallback masks a broken key (or a disabled fallback hard-fails) By default, if your BYOK key fails, Mesh **falls back to platform credentials** and the request still succeeds — billed to your Mesh balance, not your provider. A key that's been broken for weeks can go unnoticed this way. * To verify a key actually works, temporarily set the key to **Exclusive Use** (fallback disabled) and watch for failures. * With **Exclusive Use** on, a key failure fails the request with the provider's error from the table above (for example `401 upstream_auth_error` or `429 upstream_rate_limit_error`). If routing would otherwise move the request to another provider on Mesh's keys, you get `403 byok_fallback_disabled` instead. There is no safety net. Expected behavior, not a bug. Don't retry it unchanged: fix the key. ## 403 byok\_model\_not\_allowed, or a model not using your key Your key has an [Allowed models](/docs/capabilities/byok#limit-a-key-to-specific-models) list, and the requested model is not on it. * With fallback on, the model is served on Mesh's credentials and billed to your Mesh balance. Nothing fails. The signs are `X-BYOK-Used: false` on chat completions, Responses and embeddings, and the credential shown in Dashboard → **Logs**. * With **Exclusive Use** on, the request returns `403 byok_model_not_allowed` and nothing reaches the provider. Add the model, or its brand wildcard such as `openai/*`, to the key's **Allowed models**, or clear the list to let the key serve every model. ## Unexpected BYOK charges * A **5% platform fee** applies to BYOK requests, charged on the request's cost at Mesh's list prices, not on what your provider billed you. The first 1,000,000 tokens each month, audio tokens included, are fee-free. * If fallback fired, those tokens were billed to your **Mesh balance** at platform rates, not your provider account — check Dashboard → **Logs** to see which credential served each request. * A model outside the key's **Allowed models** list is billed the same way, to your Mesh balance (see above). ## Still stuck? Check Dashboard → **Logs** for the exact provider error, then see the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # Compare Source: https://developers.meshapi.ai/debug/compare Fix multi-model compare requests that partially fail or time out. `POST /v1/chat/compare` runs one prompt across multiple models concurrently. See [Compare](/docs/capabilities/model-compare) for the full guide. ## Some models are missing from the result (partial: true) Model calls are isolated — if one fails or hits the **120s hard timeout**, the others continue and the response carries `partial: true`. Check each result's `error` / `error_code`; a partial response is expected behavior, not a failure of the whole call. ## The whole request failed with 502 all\_models\_failed If every model fails, the request fails with `502` and code `all_models_failed`. This error uses a different shape from the standard envelope: it is nested under `detail` and has no `request_id`. ```json theme={null} { "detail": { "error": { "code": "all_models_failed", "message": "All models returned errors.", "details": [ { "model": "openai/gpt-4o-mini", "error": "...", "error_code": "gateway_timeout" } ] } } } ``` Each entry in `detail.error.details` gives one model's `error` and `error_code`. Fix those (a model ID, a policy restriction, a provider outage) and retry. ## `comparison` is null The synthesis only runs when at least two models succeed. It is also `null` when you set `skip_comparison: true`, when every judge in the chain failed, or when you didn't name a `comparison_model` and your key's model policy permits none of the default judges. ## Too many / duplicate models `models` accepts **1–10** entries. Duplicates are removed automatically, so listing the same model twice won't run it twice. ## Billing is higher than expected Compare bills **N+1** usage events: one per fan-out model **plus** the synthesis/comparison call. If the comparison model fails and a fallback judge takes over, each judge attempt is recorded as its own usage event, but only the judge that produced the synthesis is charged. (For rate limits, though, the whole comparison counts as a **single** request against RPM/RPD.) Set `skip_comparison: true` to drop the comparison call if you only need raw outputs. ## Streaming behaves differently than chat There are two modes: * **With comparison** (default) — fan-out models are **non-streaming**; only the final comparison text streams token-by-token (`comparison_chunk`). * **`skip_comparison: true`** — each model streams concurrently, tagged by model name (`model_chunk` / `model_stream_done`). Watch for `comparison_fallback_used` in the `done` event if the comparison model had to fall back. ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # Embeddings Source: https://developers.meshapi.ai/debug/embeddings Fix embedding request errors, dimension mismatches, and unsupported models. Most embeddings issues are request-shape or model-capability mismatches. See [Embeddings](/docs/capabilities/embeddings) for the full reference. ## 400 model\_capability\_not\_supported The model you named isn't an embedding model. Pick one where `GET /v1/models` shows `supports_embeddings: true`. ## 400 unprocessable\_entity with model "auto" Embeddings doesn't support Auto Routing. `model: "auto"` returns `400 unprocessable_entity`; name a specific embedding model. ## 422 model missing or invalid field * `model` is required unless your key has a default model configured. * `dimensions` only works on models that **support truncation** — sending it to a model that doesn't will be rejected. * `input_type`, `instructions`, and `sparse_embedding` are provider-specific (e.g. asymmetric models, BytePlus), and a model that doesn't support them may reject the request. * `sparse_embedding` is an object (`{"type": "enabled"}`), not a boolean. `true` fails validation. ## Results seem mismatched to my inputs When you pass an array of strings, the returned `data` array is in the **same order** as your input. Match by `index`, and don't assume the API reorders or dedupes — it doesn't. ## Input too long Each model has a `context_length`. Inputs longer than that are rejected — check the model's `context_length` via `GET /v1/models` and chunk long text before embedding. *(Behavior inferred from the documented `context_length` field.)* ## Garbled vectors / wrong format `encoding_format` controls the output: `"float"` returns a numeric array, `"base64"` returns a packed string you must decode. If your vectors look like gibberish, you're probably reading `base64` output as floats. ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # Image Generation Source: https://developers.meshapi.ai/debug/image-generation Fix image generation errors, missing output, and unexpected response shapes. Image generation runs over `POST /v1/images/generations` (and `POST /v1/images/edits`). See [Image Generation](/docs/capabilities/image-generation) for the full reference. ## size / quality value rejected Valid values differ by model. `quality` is `low`/`medium`/`high` for GPT image (plus `xhigh`/`max` on `gpt-image-2.5-*`), `1K`/`2K`/`4K` for Gemini 3 image, or `auto`. `size` accepts dimensions or aspect ratios (e.g. `1024x1024`, `1536x1024`) or `auto`. `n` must be **1–10**. ## I got base64 back when I asked for a URL (Vertex AI, GPT image) Vertex AI (Gemini image, Imagen) and GPT image models return image bytes, not a hosted link. With the default `response_format: "url"` the `url` field holds a `data:image/...;base64,` URI; pass `b64_json` to get the raw base64 in `b64_json`. Imagen does **not** return token usage, so don't rely on `usage` being present. ## output\_format has no effect `output_format` (`png` / `jpeg` / `webp`) is honoured only by providers that take it: GPT image models (OpenAI, Azure) and BytePlus Seedream (`png` / `jpeg` only), among others. `output_compression` is GPT image only. Providers that don't support a field ignore it. ## Streaming request looks stuck With `stream: true` you get a `text/event-stream`. On most models the early chunks are keep-alive (`status: "processing"`) plus `: ping` comments to hold the connection open during long generations. Wait for the chunk carrying `data[].url` (or `b64_json`); the stream then ends with `data: [DONE]`. BytePlus Seedream 4.0, 4.5 and 5.0 stream natively instead. Each image arrives as an `image_generation.partial_succeeded` event with `url` or `b64_json` at the top level, and the stream ends with `image_generation.completed`, with no `data: [DONE]`. See [Streaming](/docs/capabilities/image-generation#streaming). ## 422 on an edit: operation not supported The `operation` (`edit`, `remove_background`, `upscale`, `outpaint`, `inpaint`, `mix`, `reframe`) must be one the chosen model serves. Check its `image_operations` on `GET /v1/models`. An unsupported operation returns `422`. `inpaint` also requires a `mask` the same size as the image. ## 501 Not Implemented The model's provider has no image-generation (on `/v1/images/generations`) or image-edit (on `/v1/images/edits`) support, or the model isn't an image model. Pick a model whose `output_modalities` includes `image`. ## Other edit and reference-image errors | Status | Cause | | - | - | | `415` | The image, mask or a reference image isn't PNG, JPEG or WebP | | `422` | A JSON body sent a remote `http(s)` URL. Send a `data:image/;base64,...` URL instead | | `422` | `image` (generation) or `reference_images` (edit) sent to a model without `supports_image_reference` | | `422` | A file is over its size cap. See the size caps under [Edit an image](/docs/capabilities/image-generation#edit-an-image) | ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # Images & Vision Source: https://developers.meshapi.ai/debug/images-vision Fix image input failures — URL fetches, base64 payloads, and unsupported models. Image **input** goes through `POST /v1/chat/completions` as content parts. See [Images & Vision](/docs/capabilities/images-vision) for the full guide. ## The model ignores or rejects my image URL **Not all providers accept images via public URL.** Google Vertex AI drops `http(s)` image URLs without an error: the request is billed but the model answers without the image. AWS Bedrock returns `422`. If a URL isn't working, switch to a base64 `data:` URL for maximum compatibility. ## Base64 image not recognized Include the MIME-type prefix, e.g. `data:image/jpeg;base64,iVBORw0KGgo...`. Anthropic and Bedrock routes accept a bare base64 string and treat it as JPEG; other providers need the `data:image/...;base64,` prefix to detect an image. ## Model doesn't support image input at all Only multimodal models accept images. Confirm support with `GET /v1/models` and check `input_modalities` (for image input) and `output_modalities` (for image output) before sending. ## Image understanding costs more than expected Image tokens add up. Where the model supports it, set `detail: "low"` on the `image_url` part for cheaper, lower-resolution understanding. ## Wrong content shape Image input uses an **array of content parts** in the message — a `text` part plus an `image_url` part — not a plain string `content`. Sending a string with a URL in it won't attach the image. ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # Mesh API Source: https://developers.meshapi.ai/debug/mesh-api Map a Mesh API symptom to its cause and fix — auth, limits, bad requests, and upstream errors. Most failures fall into one of a few buckets: auth, limits, bad requests, or a transient upstream error. This page maps the symptom you see to its cause and fix. Start with the [first-response checklist](#first-response-checklist), then jump to the section that matches your error. Every request has a unique `req_...` ID returned in the response headers and visible in Dashboard → **Logs**. Include it in any support message — it lets us trace the exact call. ## First-response checklist Before digging in, confirm the basics: 1. **Base URL** — requests go to `https://api.meshapi.ai`. 2. **Auth header** — `Authorization: Bearer rsk_...` (note the `Bearer ` prefix and the `rsk_` key prefix). 3. **Key is active** — check Dashboard → **API Keys**; a deactivated key returns `403`. 4. **Balance** — a zero balance or hit spend cap returns `402`. Check Dashboard → **Billing**. 5. **Model exists** — confirm the model ID is enabled via `GET /v1/models`. 6. **Read `error.code`** — the response body's `error.code` field names the exact failure. ## Error code reference Every error response carries a machine-readable `error.code` and a human message, with the `request_id` at the top level beside `error`: ```json theme={null} { "error": { "code": "rate_limit_exceeded", "message": "Requests-per-minute limit exceeded" }, "request_id": "req_a1b2c3d4" } ``` `GET /v1/errors` returns the full catalog of user-facing codes with a title, explanation and suggested fix for each. The table below covers the common ones. Some responses use a different shape. `POST /v1/messages` uses Anthropic's error envelope (see [Messages API](/docs/capabilities/messages-api#errors)); `POST /v1/chat/compare` returns `502 all_models_failed` nested under `detail` with no `request_id` (see [Compare](/debug/compare#the-whole-request-failed-with-502-all_models_failed)); and some validation errors on the images, audio, template and file endpoints return a bare `{"detail": "..."}` with no `error.code`. | Code | HTTP | Cause | Fix | | - | - | - | - | | `unauthorized` | 401 | Missing or invalid API key | Check `Authorization: Bearer rsk_...` header | | `forbidden` | 403 | Key is active but suspended or lacks scope | Verify the key is enabled and has permission | | `model_excluded` | 403 | Your organization, team or key's model policy doesn't permit this model | Ask an org admin to allow it, or pick a permitted model | | `byok_fallback_disabled` | 403 | Your own provider key couldn't serve the request and fallback to Mesh's keys is off | Fix the provider key, or enable fallback. See [BYOK](/debug/byok) | | `byok_model_not_allowed` | 403 | The model is outside your provider key's allowed models and fallback to Mesh's keys is off | Add the model to the key's allowed models, or enable fallback. See [BYOK](/debug/byok) | | `spend_limit_exceeded` | 402 | Balance at zero or spend cap hit | Top up or raise the cap in **Billing** | | `rate_limit_exceeded` | 429 | RPM, RPD, or TPM limit hit | Back off and retry, or raise the key's limits | | `model_not_found` | 404 | Model ID doesn't exist or isn't enabled | Use `GET /v1/models` to list available models | | `not_found` | 404 | A non-model resource (endpoint, ID) doesn't exist | Check the URL path and any resource IDs | | `validation_error` / `unprocessable_entity` | 422 (400 for `model: "auto"` on embeddings) | Malformed request body | Fix the field named in `error.details` | | `bad_request` | 400 | The request is well-formed but can't be served as sent | Read `error.message` | | `model_capability_not_supported` | 400 | The model doesn't support this API or feature | Pick a model that supports it; check `GET /v1/models` | | `invalid_request` / `context_window_exceeded` / `content_policy_violation` | Provider's status (usually 400) | The provider rejected the request: invalid parameters, a prompt over the context window, or blocked content | Fix the request; `error.provider_error` carries the provider's own message | | `prompt_blocked` | 400 | The gateway's content shield blocked the prompt | Change the prompt | | `invalid_api_version` | 400 | `X-Mesh-Version` is malformed, unsupported, or sent empty | Send a label from `error.supported_versions`, or omit the header — see [API versioning](/docs/reference/api-versioning) | | `upstream_error` | 503 | The provider errored or was temporarily unavailable | Retry; usually transient | | `internal_error` | 500 | Unexpected gateway error | Retry; if it persists, contact support with the request ID | | `provider_job_failed` | 500 | The request reached the provider and failed in a way that may already have been charged | Don't retry blindly. The response carries `X-Should-Retry: false`; check the result first | | `upstream_rate_limit_error` | 429 | Your own (BYOK) provider key was throttled | Retry with backoff, or raise the quota on your provider account | | `upstream_auth_error` | 401 | Your own (BYOK) provider key was rejected or lacks permission | Fix the key. See [BYOK](/debug/byok) | | `insufficient_credits` | 402 | Your own (BYOK) provider account is out of credit | Top up the provider account | | `job_limit_exceeded` | 429 | Too many background jobs running | Wait for some to finish | | `provider_not_available` | 503 | No provider available for that model | Retry, or pick a different model | | `gateway_timeout` | 504 | Provider didn't respond in time | Retry — usually transient | | `stream_interrupted` | — | Streaming connection dropped mid-response | Catch and restart — partial output can't be resumed | | `parse_error` | — | SDK couldn't parse the response body | Update the SDK; retry — usually transient | The `upstream_auth_error`, `upstream_rate_limit_error` and `insufficient_credits` codes only appear when **your own** provider key ([BYOK](/docs/capabilities/byok)) served the request. When Mesh's platform key hits the same provider-side limit, you see a generic `503 upstream_error` instead. ## Reading an error: response vs. logs Every failed request surfaces in **two** places that share one `request_id` — match them to trace exactly what happened. Here's the same failure, a `429` rate limit, seen from both sides. **1. What your app receives** — the HTTP response body your code catches: ```json theme={null} { "error": { "code": "rate_limit_exceeded", "message": "Requests-per-minute limit exceeded" }, "request_id": "req_a1b2c3d4" } ``` The same ID is also returned in the `x-request-id` response header, so you can log it even before parsing the body. **2. What you see in Dashboard → Logs** — the matching row at [app.meshapi.ai](https://app.meshapi.ai) → **Logs**: ``` Time 2026-07-09 20:04:11 UTC Request req_a1b2c3d4 Status ✗ error · 429 rate_limit_exceeded Model openai/gpt-4o Endpoint POST /v1/chat/completions Latency 34 ms Tokens 812 prompt · 0 completion ``` Filter Logs by **Status: error** to list only failed calls, then open a row for its full error code and message. The `req_...` ID is the link between what your app saw and what we can trace — include it in any support message. ## Feature-specific debugging Some features have their own gotchas. If your issue is with one of these, start on its page: * [BYOK](/debug/byok) — provider key config, permissions, fallback, fees. * [Structured Output](/debug/structured-output) — `response_format` not enforced, JSON parsing. * [Auto Routing](/debug/auto-routing) — which model ran, classifier cost, latency. * [Video Generation](/debug/video-generation) — async polling, task failures, input limits. ## Common problems ### 401 unauthorized — my key is rejected The key isn't reaching the server correctly. Check, in order: * Header is exactly `Authorization: Bearer rsk_...` — the `Bearer ` prefix is required. * No stray whitespace or newline in the key (common when read from a file). * The env var is actually loaded — print its length, not its value, to confirm it isn't empty. * You're using an account key (`rsk_`), not a provider key. If a key may have leaked, deactivate it in Dashboard → **API Keys** and issue a new one immediately. ### 403 forbidden — key is valid but request is denied The key authenticated but isn't allowed to do this. Usual causes: * The key was **deactivated** or **suspended** — check its status in the dashboard. * The key lacks scope for the endpoint you're calling. * Your account is on hold for billing — see **Billing**. ### 402 spend\_limit\_exceeded — payment required Your balance is zero or a spend cap was reached. * Check **Dashboard → Billing** for current balance. * Top up, or raise the monthly/total spend cap on the key. * Spend caps can be set per key and on the organization, team or member, so a shared account can have one key blocked while others still work. The inference cost of requests served by your own provider keys (BYOK) doesn't count toward a cap, but the BYOK platform fee does, so a BYOK-only key can still get a `402`. ### 429 rate\_limit\_exceeded — too many requests You hit a rate limit. Two kinds exist: * **Your key's limits** (`rate_limit_exceeded`) — RPM, RPD, or TPM. Raise them in Dashboard → **API Keys**, or slow down. * **Provider throttling** (`upstream_rate_limit_error`) — the upstream provider throttled **your own** BYOK key. Retry with backoff. When Mesh's own provider key is throttled you see a retryable `503 upstream_error` instead. Respect the `Retry-After` header when present. All official SDKs retry `429` automatically with exponential backoff and jitter. ### 404 model\_not\_found — model unavailable The model ID is wrong or not enabled for your account. * List what's available: `GET /v1/models`. * Model IDs are case-sensitive and provider-prefixed (e.g. `openai/gpt-4o`). * Some models require [BYOK](/docs/capabilities/byok) or must be enabled first. ### 422 validation\_error — bad request body The request was well-formed JSON but a field is invalid. The `error.details` field names the offending field. Common culprits: * Missing required field (`model`, `messages`). * Wrong type — e.g. `temperature` as a string instead of a number. * Out-of-range value, for example `temperature` outside its allowed range. * `response_format` / structured-output schema is malformed. See [Structured Output](/docs/capabilities/structured-output). ### 400 invalid\_api\_version — every request fails at once Distinctive symptom: **all** requests fail identically, including ones that worked yesterday, and the body names a version rather than a field. `error.supported_versions` lists what is actually served. ```json theme={null} { "error": { "code": "invalid_api_version", "message": "API version '1999-01' is not supported. Supported versions: 2026-08, 2026-09.", "supported_versions": ["2026-08", "2026-09"] } } ``` Causes, in the order worth checking: * **The header is being sent empty.** A client that builds `X-Mesh-Version` conditionally and ends up with `X-Mesh-Version:` and no value gets rejected — an empty value is read as a malformed pin, not as "no pin". Omit the header entirely instead. * **A typo in the label.** It must be exactly `YYYY-MM`. `2026-8`, `2026-08-01`, and `v2026-08` are all rejected. * **The version is computed rather than fixed.** A label built from the current date will be wrong in almost every month; there is no version for most months. Hold it as a constant. * **An SDK pinned to a retired version.** Upgrade the SDK, or pass the current label explicitly to override its built-in one. Mesh deliberately does not fall back to a working version here, so that a bad pin fails immediately instead of quietly serving a shape you did not ask for. Validation also runs before authentication, so you get this `400` even if the key is wrong too. See [API versioning](/docs/reference/api-versioning). ### Sampling params rejected — temperature / top\_p Not every model accepts every sampling parameter, and the provider — not Mesh — enforces this. A rejected parameter comes back with the provider's own status (usually `400`, code `invalid_request`) and its message in `error.provider_error`. A value outside Mesh's own bounds is a `422 validation_error`. * **Reasoning models** reject or ignore `temperature` and `top_p`. On OpenAI's o-series and GPT-5 models Mesh drops both before forwarding, so they have no effect; steer with `reasoning_effort` instead. * **Some models reject `temperature` and `top_p` sent together** — pick one sampling strategy, not both. * Out-of-range values (`temperature` outside `0–2`, `top_p` outside `0–1`) are also rejected. When in doubt, send only `temperature` **or** `top_p`, and omit both for reasoning models. ### 5xx upstream\_error / gateway\_timeout — server-side failure These come from the upstream provider or a timeout, and are almost always transient. * **Retry with exponential backoff** — the SDKs do this for `500`, `502`, `503`, `504`. Don't retry a response marked `X-Should-Retry: false`. * If one model fails repeatedly, try [Auto Routing](/docs/capabilities/auto-routing) or a different model. * Persistent `provider_not_available` means no provider can serve that model right now — pick another. ### Streaming stops or drops mid-response Streams do **not** auto-retry. A dropped connection raises an error with `error_code="stream_interrupted"` (`MeshAPIError` in the SDKs). * Catch it and restart a fresh request — partial output cannot be resumed. * Mid-stream provider errors arrive as SSE frames before `[DONE]` and raise the same error type. * Behind a proxy or load balancer, disable response buffering so tokens flush in real time. ### Requests hang or time out * Raise the client `timeout` — reasoning and long-generation calls can take tens of seconds. * Check network egress: the API is reachable at `https://api.meshapi.ai:443`. * Streaming feels stuck? A buffering proxy is the usual cause — see the streaming section above. ### CORS errors in the browser Calling the API directly from browser or mobile client code exposes your `rsk_` key and triggers CORS failures. **Never call Mesh API from client-side code.** Route requests through your own backend proxy that holds the key. See [Authentication → Security Best Practices](/docs/getting-started/authentication). ### Responses are slow or latency is high * First token latency depends on the model and provider load — try a faster model or [Auto Routing](/docs/capabilities/auto-routing). * Use **streaming** so users see tokens as they generate instead of waiting for the full response. * For bulk, non-interactive work use the [Batch API](/docs/capabilities/batch-api) — cheaper and built for throughput. * Large RAG contexts add latency; trim retrieved chunks. See [RAG](/docs/capabilities/rag). ### Unexpected charges or usage * Every call is logged with its cost in Dashboard → **Logs**. Filter by key, model, or date. * Set **spend caps** per key to bound cost. * Streaming and non-streaming are billed identically — on tokens used. ## Handling errors in code All official SDKs raise a typed error carrying the status, error code, and request ID: ```python theme={null} from meshapi import MeshAPIError try: client.chat.completions.create(params) except MeshAPIError as e: print(f"[{e.status}] {e.error_code}: {e}") print("Request ID:", e.request_id) if e.error_code == "rate_limit_exceeded": print(f"Retry after {e.retry_after_seconds}s") elif e.error_code == "spend_limit_exceeded": print("Balance exhausted — top up to continue.") ``` See [Python → Error Handling](/sdk/error-handling). ```typescript theme={null} import { MeshAPIError } from "meshapi"; try { await client.chat.completions.create(params); } catch (e) { if (e instanceof MeshAPIError) { console.error(`[${e.status}] ${e.errorCode}`, e.requestId); } } ``` See [Node.js → Error Handling](/sdk/error-handling). ```go theme={null} resp, err := client.Chat.Completions.Create(ctx, params) if err != nil { var apiErr *meshapi.MeshAPIError if errors.As(err, &apiErr) { log.Printf("[%d] %s req=%s", apiErr.Status, apiErr.Code, apiErr.RequestID) } } ``` See [Go → Error Handling](/sdk/error-handling). ## Still stuck? Email **[contact@meshapi.ai](mailto:contact@meshapi.ai)** with: 1. The **request ID** (`req_...`) from the response headers or Dashboard logs. 2. The **model** and **endpoint** you called. 3. The **error code and message** you received. 4. The **account email** tied to your API key. More options on the [Support](/docs/reference/support) page. # Mesh CLI Source: https://developers.meshapi.ai/debug/mesh-cli Fix Mesh CLI install, sign-in, and runtime problems. New to the CLI? Start with [Install the Mesh CLI](/docs/cli/install) and [Using the Mesh CLI](/docs/cli/usage). If something isn't working, find your symptom below. ## 'meshapi: command not found' / 'not recognized' The install worked, but your terminal hasn't picked up the new PATH — almost always because the **close-and-reopen** step was skipped. 1. Close the Terminal / PowerShell window completely and open a new one, then try `meshapi` again. 2. Still failing? Re-run `pipx ensurepath`, then open **another** new window: ```bash theme={null} # macOS python3 -m pipx ensurepath # Windows py -m pipx ensurepath ``` ## 'py' or 'python3: command not found' Python isn't installed, or (on Windows) the PATH checkbox wasn't ticked during install. * Redo [Install → Step 1](/docs/cli/install). * On Windows, make sure you tick **"Add python.exe to PATH"** on the first installer screen before clicking Install. ## pipx mentions a missing 'uv' backend Install with the pip backend instead: ```bash theme={null} pipx install meshapi-code --backend pip ``` ## Wrong key or typo Re-enter your API key from inside the CLI — no need to restart: ``` /login ``` Then paste the correct `rsk_...` key. Grab a fresh one from [app.meshapi.ai](https://app.meshapi.ai) → **API Keys** if needed. ## 'Insufficient balance' (402) Your account balance is at zero, or the key hit its spend cap. * Top up at [app.meshapi.ai](https://app.meshapi.ai), or raise the key's spend cap under **API Keys** (an org or team cap needs your org admin). * A cheaper model does not help: every paid model needs a balance above zero. Only models marked free still run. This is the same `spend_limit_exceeded` / `402` you'd see from the API — see [Mesh API debugging](/debug/mesh-api#error-code-reference). ## Updating to a newer version The CLI offers new versions by itself — just press `y` when prompted. To update manually, quit with `/exit` and run: ```bash theme={null} pipx upgrade meshapi-code ``` Confirm with `meshapi --version`. See [Staying up to date](/docs/cli/commands#staying-up-to-date) for details. ## It won't write files or run commands automatically That's the **permission mode**, shown in the colored bar at the bottom. * **default** (green) asks before every file write, command, or search — that's why it keeps prompting. * Press **Shift+Tab** to cycle to **accept edits**, **auto**, or **bypass permissions** for less prompting. * Even in **bypass permissions**, genuinely dangerous actions (`rm -rf`, `sudo`, writing to `~/.ssh`) still stop and ask. See [Commands & permission modes](/docs/cli/commands#permission-modes). ## What do the y / a / n prompts mean? When the CLI wants to touch your computer it asks to **approve tool call**: * **`y`** — allow this once. * **`a`** — allow and don't ask again for **this tool** for the rest of the session. * **`n`** — deny. If you approved with `a` and want prompts back, restart the CLI or switch to a stricter permission mode. ## 'No models match' or /model won't switch * Type `/model ` then start typing — fuzzy suggestions appear; pick with arrows + Enter, or type the full ID (e.g. `openai/gpt-4o-mini`). * `/models` browses the full catalog with prices; `/models ` narrows it. A filter matching nothing prints `No models match ''` — try a broader term. * Prefer not to choose? `/route auto` lets Mesh pick per prompt (see [Auto Routing](/docs/capabilities/auto-routing)). ## Pasting the key or text doesn't work (Windows) In PowerShell, paste with **right-click**, not `⌘/Ctrl+V`. When pasting your API key on first launch, **nothing shows on screen** — it's hidden for security. Type your input and press Enter. ## /memory or /cost look wrong * `/memory` shows what the CLI has learned about the **current project** — it's per-folder, so running from a different directory shows different memory. * `/cost` is spend for the **current session** only; for full history use Dashboard → **Logs**. ## Connection / network errors The CLI talks to `https://api.meshapi.ai`. Behind a corporate proxy or VPN, make sure that host is reachable. A `402` inside the CLI is a balance issue, not a network one — see the balance entry above. ## Still stuck? Email **[contact@meshapi.ai](mailto:contact@meshapi.ai)** with the command you ran, the full output, and your CLI version (`meshapi --version`). More options on the [Support](/docs/reference/support) page. # Mesh Code Source: https://developers.meshapi.ai/debug/mesh-code Fix Mesh Code install, sign-in, and runtime problems. New to Mesh Code? Start with [Install Mesh Code](/docs/mesh-code/install) and [Using Mesh Code](/docs/mesh-code/usage). If something isn't working, find your symptom below. ## 'no build is published yet for …' The installer found no build for your OS and CPU in the current release. It lists the builds that do exist. On Windows the message reads 'no Windows build is published yet'. On 32-bit Windows the installer stops earlier with 'Mesh Code requires 64-bit Windows.' Builds are published for macOS on Apple Silicon, Windows x64, and Linux x86\_64. Intel Macs, 32-bit Windows, and ARM Linux have no build. Watch [GitHub Releases](https://github.com/aifiesta/mesh-code/releases) for new platforms, or use the [Mesh CLI](/docs/cli/install), which runs anywhere Python does. On an Apple Silicon Mac, a Terminal running under Rosetta reports `x86_64`, so the installer looks for an Intel build. Run `uname -m`. If it prints `x86_64`, clear **Open using Rosetta** on Terminal (Finder → Applications → Utilities → Terminal → Get Info), reopen Terminal, and run the installer again. ## Linux: the app won't start AppImages need libfuse2. The installer warns when it is missing. On Ubuntu 24.04 and later: ```bash theme={null} sudo apt install libfuse2t64 ``` On older Ubuntu and on Debian, install `libfuse2`. Or start the app without FUSE: ```bash theme={null} ~/.local/bin/mesh-code --appimage-extract-and-run ``` If `mesh-code` is not found, `~/.local/bin` is not on your `PATH`. Run `~/.local/bin/mesh-code` directly or add the directory to your `PATH`. ## macOS: "Mesh Code is damaged and can't be opened" You opened a `.dmg` downloaded in a browser or received over AirDrop, Slack, or Messages. macOS quarantines those, and the build is not notarised yet, so the only button offered is **Move to Bin**. Either reinstall with the installer, which avoids the quarantine: ```bash theme={null} curl -fsSL https://code.meshapi.ai/install.sh | sh ``` or check the download, then clear the flag. Compare the output of ```bash theme={null} shasum -a 256 ~/Downloads/Mesh-Code-arm64.dmg ``` with the `Mesh-Code-arm64.dmg` line in that release's `SHA256SUMS` on the [Releases page](https://github.com/aifiesta/mesh-code/releases). Only if they match, copy the app to Applications and run: ```bash theme={null} xattr -dr com.apple.quarantine "/Applications/Mesh Code.app" ``` ## 'checksum mismatch … refusing to install' The download did not match the release's published checksum, so nothing was installed. Re-run the installer. If it fails again, download the file from [Releases](https://github.com/aifiesta/mesh-code/releases) and check it against that release's `SHA256SUMS` with `shasum -a 256` (macOS), `sha256sum` (Linux), or `Get-FileHash` (Windows). If it still does not match, do not install it, and email [contact@meshapi.ai](mailto:contact@meshapi.ai). ## Wrong key or typo Open the **Account** panel (or the account menu in the sidebar) and click **Replace API key**. Paste the correct `rsk_...` key and click **Save key**. The key is checked against Mesh API before it is saved, so a bad key fails there. Get a fresh one from [app.meshapi.ai](https://app.meshapi.ai) → **API Keys**. The dialog shows the key in use as a hint (`…a1b2`) and where it came from: * **environment**: `MESHAPI_API_KEY` (or `MESH_API_KEY`) is set in the environment Mesh Code was started from, and wins over a saved key. Unset it, then restart Mesh Code. * **meshapi CLI**: the key was picked up from the Mesh CLI. Saving a key here replaces it for Mesh Code only; the CLI keeps its own. * **this app**: a key you saved in Mesh Code. **Sign out** removes only the key Mesh Code saved. If the Mesh CLI is signed in, Mesh Code falls back to the CLI's key. ## Requests fail with 402 Your account balance is at zero, or the key hit its spend cap. * Top up at [app.meshapi.ai](https://app.meshapi.ai), or raise the key's spend cap under **API Keys** (an org or team cap needs your org admin). * A cheaper model does not help: every paid model needs a balance above zero. Only models marked free still run. Find them with **Free only** in the model browser. This is the same `402` `spend_limit_exceeded` you'd see from the API. See [Mesh API debugging](/debug/mesh-api#error-code-reference). The totals in the Account panel are this app's own tally, not your balance; check the dashboard for that. ## An approval vanished, or a turn stopped early * **An approval timed out.** An approval left unanswered for 15 minutes counts as a rejection, so that step does not run. Answer it before then. * **'stopped after N hops'.** The turn hit **Max steps per turn**. Open Settings (**⌘ + ,**) and set it to `0` (unlimited), or raise it. ## It keeps asking, even in Auto or Bypass The safety checks run in every mode. File reads and writes under sensitive paths such as `~/.ssh`, and commands that match dangerous patterns such as `rm -r` or `sudo`, come back as a question with the reason stated, even in Bypass. Writes outside the project ask in Accept edits and Auto. In Auto, reading a file and starting a dev server also ask. The checks are a heuristic on the file path and the command text, not a sandbox. See [Safety checks](/docs/mesh-code/usage#safety-checks). ## vim, top, or other full-screen programs hang in the Shell panel The Shell panel is a pipe, not a terminal (PTY), so programs that take over the screen do not run there. Use your own terminal for them. ## The model browser is empty The catalog has not loaded. Check that your key is valid (see above) and that `https://api.meshapi.ai` is reachable from your network, then use **Settings › Model → Refresh**. ## Upgrading Re-run the install command for your OS. It installs the current release over the existing app. See [Install Mesh Code](/docs/mesh-code/install#install). ## Still stuck? Email **[contact@meshapi.ai](mailto:contact@meshapi.ai)** with your OS, what you did, and what you saw. **Send feedback** in the sidebar also opens a short form. More options on the [Support](/docs/reference/support) page. # Prompt Templates Source: https://developers.meshapi.ai/debug/prompt-templates Fix templates that don't render, don't resolve, or don't apply the model you expected. Prompt Templates inject `{{variable}}` values into a stored system prompt and base messages. See [Prompt Templates](/docs/capabilities/prompt-templates) for the full guide. ## 422 — missing variables at inference time Every `{{variable}}` slot the template renders must be supplied in the `variables` object at inference time. A missing one returns a **`422`**. Extra variables you send are silently ignored. ## Variables aren't being substituted * Syntax is double-brace: `{{ variable_name }}` (whitespace inside is trimmed, so `{{company}}` == `{{ company }}`). * Names may contain letters, digits, and underscores only. * The template's `variables` list is **informational** — it documents expected slots but isn't enforced server-side, so a typo in a slot name won't be caught for you. ## Template not found / wrong template used * **Name** lookups resolve to your key's owner first, then fall back to global (admin-seeded) templates. If you own a template with the same name as a global one, yours wins. * **UUID** lookups work across any owner — use the ID to share a template with another team. * A name that matches nothing (and no global) fails to resolve. * A template with **no published version** returns `404`. Creating a template publishes version 1, so check that your newer draft was published too (`POST /v1/templates/{id}/publish`). * A `template_version` pin returns `404` if that version number doesn't exist or was never published. * An edit you saved as a new version doesn't serve until you publish it. Requests use the published version. ## My per-request params aren't taking effect Parameters resolve highest-wins: **request body > key defaults > template defaults**. If a value isn't changing, something higher in that order is overriding it — e.g. a key default beating your template value. ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # RAG (Files & Search) Source: https://developers.meshapi.ai/debug/rag Fix file uploads, stuck embeddings, and searches that return nothing. RAG is a three-step flow — **upload → embed → search** — and most issues come from acting before a step finishes. See [RAG](/docs/capabilities/rag) for the full guide. ## Upload 'works' but the file is empty / not searchable Uploading is **two calls**: `POST /v1/files` registers the file and returns a `signed_url`; you then **PUT the actual bytes** to that URL. * If you skip the PUT (or it failed), the file exists but has no content. * The PUT goes to storage directly — **don't** send your `Authorization` header on it. * Send every header in the init response's `required_headers` exactly as given. They are signed, so a missing or altered one gets the PUT rejected. ## upload\_status is failed Mesh checks the bytes after they land. `upload_status` becomes `failed` when: * the file is over 50 MB (`max_size_bytes` in the init response), or * the PUT's `Content-Type` doesn't match the `mime_type` you registered. Register the file again and redo the PUT. A poll loop that only watches `embedding_status` never ends in this case, so check `upload_status` too. Give the loop a deadline as well: if the PUT never lands, `upload_status` stays `pending` forever. ## 'signed\_url' rejected / expired The `signed_url` expires **15 minutes** after you register the file. PUT your bytes right away. If it expired, register the file again to get a fresh URL. ## Search returns nothing right after upload Embedding is asynchronous. A file is only searchable once `embedding_status` is `ready`. * Poll `GET /v1/files/{file_id}` until `embedding_status` is `ready` (it moves `queued → processing → ready`). * If it's `failed`, re-trigger with `POST /v1/files/embed` (pass `"wait": true` to block for small files). The file's `upload_status` must be `ready`, and a file that failed can't be re-queued for 30 minutes. * If `embed: false` was set at upload, embedding never started — trigger it manually. ## Search returns irrelevant or too few results * Raise `top_k` (1–50, default 5). * An over-restrictive `filter` (metadata is **exact match**) or `date_from`/`date_to` window can exclude everything — loosen them. * A too-narrow `file_ids` list restricts search to those files only; omit it to search all your files. * Metadata filters only match keys you set **at upload time**. ## 422 on upload Check required fields on `POST /v1/files` — `file_name` and `mime_type` (e.g. `application/pdf`, `text/plain`, `text/markdown`). The `mime_type` also drives file-type detection; an unsupported one is a `422`. ## 400 on upload: key owner is not a valid UUID RAG needs a key created in the dashboard. A key minted another way (for example with an admin key) has no user owner, and uploads with it return `400`. ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # Realtime Audio Source: https://developers.meshapi.ai/debug/realtime-audio Fix realtime WebSocket sessions — handshake, auth, audio framing, and disconnects. Realtime audio is a WebSocket gateway at `wss://api.meshapi.ai/v1/realtime`. Most issues are connection, auth, or event-shape problems. See [Realtime Audio](/docs/capabilities/realtime-audio) for the full reference. ## Can't connect over HTTP This endpoint exists only as a WebSocket upgrade. A plain HTTP request to `/v1/realtime` is not served. Connect with a WebSocket client. Also, TLS is mandatory: `ws://` is rejected, use `wss://`. ## Auth fails, especially in the browser Two methods: the `Sec-WebSocket-Protocol: openai-realtime, Bearer ` subprotocol (preferred), or `?api_key=` on the URL. * Browsers can't set request headers on a WebSocket and can't reliably send the space-separated `Bearer` subprotocol — use the `?api_key=` query fallback. * If both are present, the subprotocol wins. * An `Authorization` header is not read on this endpoint. A client that only sets one (e.g. `websocat -H "Authorization: ..."`) gets `invalid_api_key`. ## session.update is rejected Realtime uses OpenAI's **GA** event protocol. Use the GA shape (`{"type":"realtime","output_modalities":[...],"audio":{...}}`). The older **beta** shape (`modalities` / `voice` / `input_audio_format`) is rejected. ## Audio isn't received Audio frames are **base64 inside text events** (`input_audio_buffer.append` up, `response.output_audio.delta` down), as OpenAI's protocol defines. Mesh forwards binary WebSocket frames unchanged, but the protocol doesn't read raw PCM sent that way. Send audio as `input_audio_buffer.append` events. ## Session drops after a while * **30-minute cap.** Mesh closes the session with `session_duration_exceeded`. Reconnect and resume app-level state for long agents. * **60s idle timeout.** Mesh closes a socket with no client→server frame for 60s, with `idle_timeout`. Keep the audio buffer flowing or send a `session.update` ping. * **`service_draining` / `connection_timeout`.** The server restarted or the heartbeat was lost. Reconnect. ## Reading errors — use error.code, not the close code Errors arrive as a JSON frame (`{ "type": "error", "error": { "code", "message" }, "request_id" }`) just before the socket closes. Branch on `error.code` — the close code is incidental. | `error.code` | Meaning | | - | - | | `invalid_api_key` | Missing, malformed, or revoked `rsk_...` key. | | `insufficient_quota` | Balance can't cover the session hold, or ran out. Top up to reconnect. | | `concurrent_sessions_exceeded` | Too many open sessions (5 per account by default) or the server is at capacity. | | `rate_limit_exceeded` / `spend_limit_exceeded` | An organization, team or member rate limit or spend cap. | | `session_token_cap_exceeded` | Session hit its configured token cap. | | `model_not_found` | Model unavailable to your account or not realtime-capable. | | `provider_error` | Upstream provider error — read `error.message`. | More codes, including the lifecycle ones, are in the [error code reference](/docs/capabilities/realtime-audio#error-code-reference). Close codes seen: `1000` (normal closure, including auth, quota and limit errors) and `1011` (server/upstream conditions). ## Session won't open / billing surprises * Opening a session holds up to \$10 of your balance until it closes; if your balance can't cover the hold, you get `insufficient_quota`. * Sessions cut short (network drop, tab close) **still bill** for tokens already processed. * There's no in-stream cost message. Usage is settled every 60 seconds and at close. Query `GET /v1/usage` for canonical numbers. ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # Speech-to-Text Source: https://developers.meshapi.ai/debug/speech-to-text Fix transcription failures — file formats, size limits, and model support. Transcription runs over `POST /v1/audio/transcriptions` (REST) and `WS /v1/audio/transcriptions/realtime` (streaming). See [Speech-to-Text](/docs/capabilities/speech-to-text) for the full reference. ## 422 — the endpoint expects multipart/form-data, not JSON `POST /v1/audio/transcriptions` uses **`multipart/form-data`**, not a JSON body. Send fields as form parts (`-F` in curl, `files=`/`data=` in httpx). Posting JSON is the most common cause of a 422 here. ## No audio provided Supply an input source: a `file` upload, a `source_url` (public URL), or a `cloud_storage_url` (S3/GCS). Omitting all three returns `422` with "One of file, source\_url, or cloud\_storage\_url is required." ## 422 on transcribe-and-translate `POST /v1/audio/transcriptions/translate` (a legacy alias of `POST /v1/audio/translations`) requires a model that **supports translation**. Always pass `model` explicitly, e.g. `openai/whisper-large-v3`. If the selected model can't translate, you get a `422`. Confirm capabilities via `GET /v1/models`. ## Opting out of logging has no effect `enable_logging` is a query parameter on `POST /v1/audio/transcriptions` (`?enable_logging=false`), not a form field. Sent as a form field it is ignored. ## Realtime WebSocket won't connect or auth fails Pass the key one of three ways: `Sec-WebSocket-Protocol: Bearer rsk_...`, `?api_key=rsk_...`, or `?token=rsk_...`. In browsers (no custom headers), use the query parameter. ## Realtime transcripts are empty or garbled * `audio_format` must match what you actually send (default `pcm_16000`; the standard protocol expects **PCM `s16le`, 16 kHz, mono**). * With `commit_strategy: manual` you must send `input_audio_buffer.commit` (or ElevenLabs `commit: true`) to get a final transcript; `auto` uses VAD. * **ElevenLabs realtime** only forwards `input_audio_chunk` frames — any other message type is silently dropped. ## WebSocket closed with an error Realtime STT sends a JSON `error` frame and then closes with code `1000`, or `1011` while the server is draining for a restart, in which case reconnect. Read the `error` frame for the reason (auth, balance, too many concurrent sessions, or upstream). An account can hold 5 concurrent WebSocket sessions. REST errors use standard codes: `401`, `422`, `429`, `402`. ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # Structured Output Source: https://developers.meshapi.ai/debug/structured-output Fix schema rejections, malformed JSON, and models that ignore response_format. Structured output depends on the **model** honoring `response_format`. The most common surprise is that a model silently ignores it. See [Structured Output](/docs/capabilities/structured-output) for the full guide. ## I get plain text instead of JSON (no error) **Not every model enforces `response_format`.** On many models that don't support it, the request still succeeds and returns ordinary text. It does not error. On Amazon Bedrock, `response_format` is also dropped when the request carries `tools`. * Switch to a model with first-class support, e.g. OpenAI models or `google/gemini-2.5-flash`. * Always parse defensively and handle the case where the content isn't valid JSON. ## 4xx response\_format rejected Mesh forwards `response_format` without validating its contents, so a bad one is rejected by the provider (usually `400`), not by Mesh. Only a `response_format` that isn't a JSON object gets a `422` from Mesh. Check: * `type` is one of `"text"`, `"json_object"`, `"json_schema"`. * For `json_schema`, the `json_schema` object has both a `name` and a `schema`. * The `schema` is valid JSON Schema (no trailing commas, correct types). * The model supports `json_schema`. DeepSeek, Arcee and ILMU accept only `json_object`. ## Valid JSON but the wrong keys `json_object` only guarantees **syntactically valid JSON** — not which keys appear. The structure follows your prompt, not a contract. * Describe the exact keys you expect in the prompt, **or** * Use `json_schema` for a guaranteed shape. Set `additionalProperties: false` to forbid extra fields, and list every required field in `required`. ## JSON is truncated / won't parse If the output is cut off mid-object, `max_tokens` was too low — the model ran out of room before closing the JSON. * Raise `max_tokens`. * Check `finish_reason`: `"length"` means truncated, `"stop"` means complete. ## content is a string, not an object The JSON is returned as a **string** in `choices[0].message.content`. You must parse it yourself: ```python theme={null} data = json.loads(resp.choices[0].message.content) ``` ```ts theme={null} const data = JSON.parse(resp.choices[0].message?.content ?? "{}"); ``` ## Output isn't deterministic across runs Sampling introduces variation even with a schema. * Set `temperature: 0` for the most deterministic, schema-faithful output. * Note that for Gemini models, `response_format` is translated server-side to Gemini's native `responseMimeType` / `responseSchema`, so enforcement happens on the provider — behavior can differ subtly from OpenAI. ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # Text-to-Speech Source: https://developers.meshapi.ai/debug/text-to-speech Fix speech synthesis errors, voice lookup failures, and audio format problems. TTS runs over `POST /v1/audio/speech` (REST) and `WS /v1/audio/speech/stream/{voice_id}` (streaming). See [Text-to-Speech](/docs/capabilities/text-to-speech) for the full reference. ## The response won't parse as JSON `POST /v1/audio/speech` returns **raw audio bytes**, not JSON. The `Content-Type` matches the requested format (e.g. `audio/mpeg`, `audio/wav`). Write the body straight to a file — don't call `.json()` on it. ## Voice errors / wrong or missing voice * `voice` is **required for ElevenLabs models** and must be a valid ID for that model's brand. * A voice from one brand won't work with a model from another. Browse valid IDs with `GET /v1/audio/voices` and **filter by `brand` or `model`** so you only pick voices usable with the model you call. * Sarvam models (`sarvam/bulbul:v3`) use `speaker` (default `ritu`), not `voice`. ## response\_format rejected Not every format is valid in every mode. `wav_*` formats are **non-streaming only** — request them with `stream: false`. Streaming supports `mp3_*`, `pcm_*`, `ulaw_8000`, `alaw_8000`, and `opus_*`. ## WebSocket streaming produces no audio The frame protocol depends on the model family: * **Standard models** (Kokoro, Cartesia, …) — send `input_text_buffer.append` then `input_text_buffer.commit`; audio arrives as `conversation.item.audio_output.delta` (base64). * **ElevenLabs models** — the **first** frame must be `initializeConnection` (`{ "text": " " }`), then `sendText`, then `closeConnection` (`{ "text": "" }`); audio arrives as `AudioOutput` (base64). Audio deltas are base64 — decode before playing or writing. ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # Video Generation Source: https://developers.meshapi.ai/debug/video-generation Fix video generation task failures, stalled polling, and expired result URLs. Video generation is **asynchronous** — the create call returns a task ID, not a video. Most issues come from treating it like a synchronous endpoint, or from input constraints on the model. See [Video Generation](/docs/capabilities/video-generation) for the full guide. ## The POST response has no video, just an id That's expected. `POST /v1/video/generations` returns `{"id": "t-..."}` immediately. The video isn't ready yet. * Poll `GET /v1/video/generations/{id}` until `status` is terminal, **or** * Pass a `callback_url` to receive the result by webhook. BytePlus, Kling and MiniMax models only. Any other model returns `422` for a `callback_url`; poll it instead. ## HTTP 200 but the task failed A task can fail *after* it was accepted. In that case the GET returns **HTTP 200** with `status: "failed"` and an `error` object — the failure is in the body, not the HTTP status. ```json theme={null} { "status": "failed", "error": { "code": "content_policy_violation", "message": "..." } } ``` Always branch on `status` (`succeeded` / `failed` / `expired` / `cancelled`), not just the HTTP code. Mesh's own `error.code` values include `content_policy_violation`, `video_generation_failed`, `upstream_error`, `provider_video_failed` and `task_abandoned`. Any other code is the provider's own. The full table is in [A task that failed after it was accepted](/docs/capabilities/video-generation#a-task-that-failed-after-it-was-accepted). ## List endpoint shows a stale status `GET /v1/video/generations` (the list) is served from Mesh's database and does **not** refresh from the provider. For a live status on an in-progress task, call `GET /v1/video/generations/{id}` — that one forces an upstream sync. ## Video / audio input rejected Which inputs a model takes varies model by model, and the capability flags on `GET /v1/models` are the answer — `supports_video_reference_video` and `supports_video_reference_audio` for these two. See [Finding out what a model accepts](/docs/capabilities/video-generation#finding-out-what-a-model-accepts). * A clip or an audio track is accepted by far fewer models than an image is. Check the flag before you rely on one. * Some models will not take **audio as the only media** — send a reference image or a clip with it. * Use `duration` **or** `frames`, not both. ## Request too large / Base64 failures **Total request body: 32 MiB (33,554,432 bytes)** — a hard platform limit on every Mesh endpoint. Requests above it are rejected at the edge *before* they reach the API, so the response is a plain HTML `413 Request Entity Too Large` page with no Mesh error envelope and no request ID. See [Rate Limits & Spend Caps](/docs/getting-started/rate-limits#request-body-size). Per-file caps are the model's, and they are only reachable via public URLs, not Base64 — see [What you can send](/docs/capabilities/video-generation#what-you-can-send) for the widest ceilings. Base64 inflates a payload by \~33%, so a **\~24 MB** file on its own already exceeds the 32 MiB body limit. Do **not** Base64-encode large files. Use a public URL instead — it must be reachable **without authentication**, since the model fetches it — and if you do inline small files, the data URI must include the MIME prefix (e.g. `data:video/mp4;base64,...`). ## 422 on the create call Two different causes, and the message tells them apart. **The `model` ID isn't a video-generation model.** List the ones that are with `GET /v1/models?type=video`, or check `supports_video_generation` on the model you had in mind. **The model can't take one of your `content` items** — the message names it and says it "would be dropped and the generation billed anyway". `supports_video_generation` is `true` here; the flag you need is the one for that input, e.g. `supports_video_last_frame`. Check the six on `GET /v1/models`, or `GET /v1/models/{model_id}/providers` to see whether any route takes it. Full detail: [Finding out what a model accepts](/docs/capabilities/video-generation#finding-out-what-a-model-accepts). ## Task shows 'expired' The task didn't finish before its expiry window. On BytePlus models the default is `execution_expires_after` = **172800 s (48 h)**; raise it on the create request if you expect long jobs, or resubmit. Other providers ignore `execution_expires_after` and apply their own window. ## Task failed with `task_abandoned` The provider never reported a terminal status. On providers without webhooks (anything but BytePlus, Kling and MiniMax), Mesh closes a task that is still unfinished an hour after creation and does not charge for it. Resubmit. ## My webhook never fired Callbacks are **fire-and-forget** with a **10-second timeout** and are **not retried**. * Return a `2xx` quickly (do heavy work after responding). A slow or failing endpoint means you miss the event. * Only BytePlus, Kling and MiniMax models take a `callback_url`. Any other model is refused with `422` at create time. * Kling and MiniMax call back at most once, on a terminal state (`succeeded`, `failed`, `expired`). If a poll (yours or Mesh's own background check) settles the task first, no callback is sent. BytePlus forwards every status update, so ignore non-terminal deliveries such as `running`. * Treat polling `GET /v1/video/generations/{id}` as the source of truth. Mesh deduplicates usage logging, so you're never billed twice. ## 5xx from the video service * `502` — an upstream error while generating. `503` — video generation temporarily unavailable. Both are usually transient; retry with backoff. ## Still stuck? See the [Mesh API error reference](/debug/mesh-api#error-code-reference) or email **[contact@meshapi.ai](mailto:contact@meshapi.ai)**. # Audio Source: https://developers.meshapi.ai/docs/capabilities/audio Send audio into chat completions and request audio output from supported models. Mesh API supports audio through `POST /v1/chat/completions`. Use this page for: * audio input with `input_audio` * audio output with `modalities` and `audio` ## Audio input Send audio as a content part inside a chat message. ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemini-3-flash-preview", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Transcribe this clip." }, { "type": "input_audio", "input_audio": { "data": "", "format": "wav" } } ] } ] }' ``` ```ts theme={null} const response = await client.chat.completions.create({ model: "google/gemini-3-flash-preview", messages: [ { role: "user", content: [ { type: "text", text: "Transcribe this clip." }, { type: "input_audio", input_audio: { data: "", format: "wav", }, }, ], }, ], }); ``` ```python theme={null} from meshapi import ChatCompletionParams, ChatMessage, ContentPartAudio, ContentPartText, InputAudio, MeshAPI client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") response = client.chat.completions.create( ChatCompletionParams( model="google/gemini-3-flash-preview", messages=[ ChatMessage( role="user", content=[ ContentPartText(type="text", text="Transcribe this clip."), ContentPartAudio( type="input_audio", input_audio=InputAudio(data="", format="wav"), ), ], ) ], ) ) ``` ```go theme={null} model := "google/gemini-3-flash-preview" prompt := "Transcribe this clip." audioData := "" params := meshapi.ChatCompletionParams{ Model: &model, Messages: []meshapi.ChatMessage{ { Role: "user", Content: []meshapi.ContentPart{ {Type: "text", Text: &prompt}, { Type: "input_audio", InputAudio: &meshapi.InputAudio{ Data: &audioData, Format: "wav", }, }, }, }, }, } _, err := client.Chat.Completions.Create(ctx, params) ``` ```java theme={null} MeshAPI client = MeshAPI.builder() .baseUrl("https://api.meshapi.ai") .token("rsk_...") .build(); ChatCompletionRequest request = ChatCompletionRequest.builder() .model("google/gemini-3-flash-preview") .addMessage( ChatMessage.builder() .role("user") .content(java.util.List.of( java.util.Map.of("type", "text", "text", "Transcribe this clip."), java.util.Map.of( "type", "input_audio", "input_audio", java.util.Map.of("data", "", "format", "wav") ) )) .build() ) .build(); client.chat().completions().create(request); ``` `input_audio` takes base64-encoded bytes in `data`, or a remote `url` on BytePlus models, plus a `format` (`wav`, `mp3`, `aiff`, `aac`, `ogg`, `flac`, `m4a`, `pcm16` or `pcm24`). Keep inline audio to about 7 MB. ## Audio output Request text and audio together when the model supports audio output. ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-audio", "messages": [ { "role": "user", "content": "Read this back to me in a calm voice." } ], "modalities": ["text", "audio"], "audio": { "voice": "alloy", "format": "wav" } }' ``` The same request shape is available through all four SDKs by setting chat-completions fields for `modalities` and `audio`. ## Translate audio to English `POST /v1/audio/translations` accepts audio in any language and returns the speech **translated to English**. It returns the same `TranscriptionResponse` (with a `.text` field) as transcription. `POST /v1/audio/transcriptions/translate` is a legacy alias of this endpoint with the same behaviour. It takes `model`, `file`, `prompt`, `tags` and `safety_identifier`, but not `response_format` or `temperature`. Use `/v1/audio/translations` and always pass `model`. Check `GET /v1/models` for models that support translation. ```python theme={null} from meshapi import MeshAPI, AudioTranslationsParams client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") with open("french_audio.mp3", "rb") as f: audio_bytes = f.read() result = client.audio.audio_translate( audio_bytes, AudioTranslationsParams(model="openai/whisper-large-v3"), filename="french_audio.mp3", ) print(result.text) # English translation ``` ```ts theme={null} import { readFileSync } from "fs"; const audio = readFileSync("french_audio.mp3"); const result = await client.audio.translations( audio, { model: "openai/whisper-large-v3" }, { filename: "french_audio.mp3" }, ); console.log(result.text); // English translation ``` ```go theme={null} audioBytes, _ := os.ReadFile("french_audio.mp3") result, err := client.Audio.Translations(ctx, audioBytes, "french_audio.mp3", meshapi.AudioTranslationParams{ Model: "openai/whisper-large-v3", }) if err != nil { log.Fatal(err) } fmt.Println(result.Text) // English translation ``` Optional parameters: `prompt` (context hint for the model), `response_format` (`json`, `text`, or `verbose_json`), and `temperature` (0–2). ## SDK coverage * Node: `client.chat.completions.create(...)` * Python: `client.chat.completions.create(...)` * Go: `client.Chat.Completions.Create(...)` * Java: `client.chat().completions().create(...)` ## Notes * Inline audio payloads are base64 encoded in the request body. BytePlus models also take a remote `url`. * Check `GET /v1/models` to find models that accept or produce audio. * Keep payload sizes reasonable, especially for browser-based clients. # Auto Routing Source: https://developers.meshapi.ai/docs/capabilities/auto-routing Dynamically route requests to the best model. Mesh API's Auto Router allows you to simply set `model: "auto"` on any inference request. The gateway classifies the request, selects the most appropriate model from the live registry, and forwards the request transparently. This requires no client-side logic beyond setting the `model` field to `"auto"`. For how the router makes the pick (the routing algorithms, the quality/cost/latency scoring formula, and the `weight_profile` knob that shifts it), see [Routing Algorithms](/docs/capabilities/routing-algorithms). ## Supported endpoints Auto Routing is supported across the following inference endpoints: | Endpoint | Streaming Supported | | - | - | | `POST /v1/chat/completions` | Yes | | `POST /v1/responses` | Yes | | `POST /v1/messages` | Yes | On `POST /v1/messages`, only streaming responses report the pick, in the `X-Resolved-Model-Id` header. A non-streaming Messages response echoes `"model": "auto"` and carries none of the `x_auto_routed` or `x_classifier_usage` fields. Check Dashboard → **Logs** to see which model served it. `POST /v1/embeddings` does not accept `"auto"`. It returns `400 unprocessable_entity`; name a specific embedding model. ## Basic request Just replace your specific model ID with `"auto"`: ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "auto", "messages": [{"role": "user", "content": "Write a Python quicksort implementation"}] }' ``` ```ts theme={null} const response = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: "Write a Python quicksort implementation" }], }); ``` ```python theme={null} from meshapi import ChatMessage, ChatCompletionParams, MeshAPI client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") response = client.chat.completions.create( ChatCompletionParams( model="auto", messages=[ChatMessage(role="user", content="Write a Python quicksort implementation")], ) ) ``` ```go theme={null} model := "auto" params := meshapi.ChatCompletionParams{ Model: &model, Messages: []meshapi.ChatMessage{ {Role: "user", Content: "Write a Python quicksort implementation"}, }, } _, err := client.Chat.Completions.Create(ctx, params) ``` ```java theme={null} ChatCompletionRequest request = ChatCompletionRequest.builder() .model("auto") .addMessage(ChatMessage.user("Write a Python quicksort implementation")) .build(); client.chat().completions().create(request); ``` ## Steering the choice By default the router balances quality, cost and latency. Send `weight_profile` to lean it one way for a single request: ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "auto", "weight_profile": "cost_first", "messages": [{"role": "user", "content": "Summarise this changelog in one line"}] }' ``` `quality_first`, `balanced` (default), `cost_first` and `latency_first` are the four profiles. You can also set a default on the API key, or on your team or organization, and still override it per request — see [Routing Algorithms](/docs/capabilities/routing-algorithms#choosing-a-profile) for the weights behind each profile and the full precedence order. `weight_profile` applies to `POST /v1/chat/completions`. An unrecognised name falls back to `balanced` rather than erroring, so check the spelling if a profile doesn't seem to be taking effect. ## Response metadata When a request is automatically routed, Mesh API injects metadata into the response so you know which model the router picked. ### Non-streaming requests The metadata is included directly in the response body. ```json theme={null} { "id": "chatcmpl-...", "model": "anthropic/claude-haiku-4.5", "choices": [...], "x_auto_routed": true, "x_resolved_model_id": "anthropic/claude-haiku-4.5" } ``` If the router could not make its own pick and fell back (see [Fallback behavior](#fallback-behavior)), additional fields are present: ```json theme={null} { "x_auto_routed": true, "x_resolved_model_id": "qwen/qwen3.5-27b", "x_auto_routed_fallback": true, "x_auto_routed_fallback_reason": "all_strategies_failed" } ``` `x_resolved_model_id` is the router's pick. If that model fails and the request moves to one of the router's runner-ups, the model that served is in the body's `model` field. Non-streaming responses also carry it in the `X-Mesh-Routing-Model` header, but only when the key's routing policy enables retry or fallback. ### Streaming requests For streaming requests (`stream: true`), the metadata is included as HTTP response headers before the SSE stream begins: ```text theme={null} X-Auto-Routed: true X-Resolved-Model-Id: anthropic/claude-haiku-4.5 ``` If a fallback was used, two further headers are present, mirroring the JSON fields above: ```text theme={null} X-Auto-Routed-Fallback: true X-Auto-Routed-Fallback-Reason: all_strategies_failed ``` `X-Auto-Routed`, `X-Resolved-Model-Id` and the fallback headers are not in the gateway's CORS `Access-Control-Expose-Headers` list, so browser JavaScript calling the API cross-origin cannot read them. Server-side clients can. ## Routing-data version The data the router ranks models with is versioned. Every response to a `model: "auto"` request carries the version it routed on in the `X-Mesh-Router-Version` header. You can pin a version, `v10` or `v12` (`v12` steers prompts that refer to an attached file or image to models that can read one). Highest precedence first: 1. The `router_version` body field 2. The `X-Mesh-Router-Version` request header 3. `router_version` in the API key's routing policy 4. The key's routing-policy template, then your organization's default template A value that isn't a selectable version is skipped, not refused; with nothing selected, the gateway's active version applies. A stored value on a key or template is validated when you save it. ## Fallback behavior The Auto Router is designed to never block a request due to its own failure. The routing algorithms run as a waterfall: each one that declines, times out or errors falls through to the next (see [Routing Algorithms](/docs/capabilities/routing-algorithms#the-algorithms)). If every algorithm misses, a configured default model serves the request. A pick made by any algorithm in the chain is a normal pick. The response carries `x_auto_routed_fallback: true` and an `x_auto_routed_fallback_reason` only when the router could not use its own pick. Common reasons: | Reason | Meaning | | - | - | | `all_strategies_failed` | Every algorithm missed; the default model served | | `exception` | The router hit an internal error; the default model served | | `entitlement_filtered` | The pick isn't permitted by your model policy; another permitted model was chosen | | `exclude_filtered` | The pick was in the request's `exclude_models`; another model was chosen | | `capability_filtered` | The request carries tools and the pick can't call them; a tool-capable model was chosen | | `no_entitled_candidates` | No routable model was left to re-pick from | You may also see `classifier_timeout`, `classifier_error`, `invalid_response` or `empty_registry`. Each means the router couldn't classify the request and the default model served. The classifier model and the default model are **different models with different prices**. A request that falls all the way through is served by the default — not by the classifier — so check `x_resolved_model_id` rather than assuming. ## Billing When using the Auto Router, you are billed for the tokens consumed by the *resolved* model that actually served the request, **as well as the tokens consumed by the internal classifier model**. Both appear in your usage dashboard. The classifier's cost is broken out on the response itself, so you can see it per request rather than only in aggregate. On `/v1/chat/completions`, `prompt_tokens`, `completion_tokens` and `total_tokens` already include the classifier's tokens; on `/v1/responses` only `classifier_tokens` is added, to `total_tokens`. ```json theme={null} { "usage": { "prompt_tokens": 1458, "completion_tokens": 30, "total_tokens": 1488, "classifier_prompt_tokens": 1446, "classifier_completion_tokens": 26, "classifier_tokens": 1472 }, "x_classifier_usage": [ { "model_id": "openai/gpt-4o-mini", "prompt_tokens": 1446, "completion_tokens": 26 } ] } ``` `x_classifier_usage` is a list because a request whose first classifying algorithm misses bills for every classifier call it made, each as its own entry. A request answered by the `heuristic` fast lane makes no classifier call. A classification is cached for 24 hours per prompt, so a repeated prompt's entry shows `0` tokens and isn't billed. *** ## When to use auto routing * **Rapid prototyping** — skip the model selection decision early in development * **Mixed-complexity workloads** — let the gateway route simple queries to cheap models and hard ones to frontier models * **A/B testing** — observe which models get selected for your actual traffic # Batch API Source: https://developers.meshapi.ai/docs/capabilities/batch-api Submit requests inline, create a batch, poll for completion, and read results inline. The Batch API is for asynchronous, high-volume inference jobs where you do not need an answer immediately. Only models whose catalog entry has `supports_batching: true` can be batched. Today these are OpenAI models (for example `openai/gpt-5.4`, `openai/gpt-4o-mini`). Check the flag in `GET /v1/models`. Any other model returns `400 model_not_supported_for_batching`. Batches always run on Mesh's platform credentials, never on your BYOK keys. ## Workflow 1. Prepare a request bundle 2. Create a batch with `POST /v1/batches` 3. Poll `GET /v1/batches/{batch_id}` — results are included inline once complete ## 1 & 2. Create the batch Pass your requests inline — no separate file upload required. ```bash theme={null} curl https://api.meshapi.ai/v1/batches \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "requests": [ { "custom_id": "doc-001", "body": { "model": "openai/gpt-5.4", "messages": [{ "role": "user", "content": "Summarize this note." }] } } ], "completion_window": "24h" }' ``` ```ts theme={null} const batch = await client.batches.create({ requests: [ { custom_id: "doc-001", body: { model: "openai/gpt-5.4", messages: [{ role: "user", content: "Summarize this note." }], }, }, ], completion_window: "24h", }); ``` ```python theme={null} from meshapi import BatchRequestItem, CreateBatchParams, MeshAPI client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") batch = client.batches.create( CreateBatchParams( requests=[ BatchRequestItem( custom_id="doc-001", body={ "model": "openai/gpt-5.4", "messages": [{"role": "user", "content": "Summarize this note."}], }, ) ], completion_window="24h", ) ) ``` ```go theme={null} window := "24h" batch, err := client.Batches.Create(ctx, meshapi.CreateBatchParams{ Requests: []meshapi.BatchRequestItem{ { CustomID: "doc-001", Body: map[string]interface{}{ "model": "openai/gpt-5.4", "messages": []map[string]string{ {"role": "user", "content": "Summarize this note."}, }, }, }, }, CompletionWindow: &window, }) ``` ```java theme={null} var item = new com.meshapi.sdk.types.batch.BatchRequestItem(); item.customId = "doc-001"; item.body = java.util.Map.of( "model", "openai/gpt-5.4", "messages", java.util.List.of(java.util.Map.of("role", "user", "content", "Summarize this note.")) ); var batchRequest = new com.meshapi.sdk.types.batch.CreateBatchRequest(); batchRequest.requests = java.util.List.of(item); batchRequest.completionWindow = "24h"; var batch = client.batches().create(batchRequest); ``` ### Request item fields Each entry in `requests` supports: | Field | Type | Notes | | - | - | - | | `custom_id` | string | Your identifier, echoed back on the matching result. **Required.** | | `body` | object | The request body (e.g. a chat completion payload). **Required.** | | `method` | string | HTTP method. Defaults to `POST`. | | `url` | string | Defaults to `/v1/chat/completions`, the only supported value. Every batch runs against chat completions. | The batch create call also accepts an optional `metadata` object (arbitrary key-value pairs) alongside `completion_window`. ### Limits * A batch may not mix models — all requests must target the same model, or the create call returns `400 mixed_models`. * You can have at most **10 batches in a non-terminal state** at once; an eleventh returns `429 batch_limit_exceeded`. ## 3. Poll and read results Poll until `status` is a terminal value. When `completed`, the response includes a `results` array — no separate file download needed. ```bash theme={null} curl https://api.meshapi.ai/v1/batches/batch_xyz789 \ -H "Authorization: Bearer " ``` ```json theme={null} { "id": "batch_xyz789", "status": "completed", "request_counts": { "total": 1, "completed": 1, "failed": 0 }, "results": [ { "custom_id": "doc-001", "response": { "status_code": 200, "body": { "choices": [{ "message": { "content": "The note says..." } }], "usage": { "prompt_tokens": 12, "completion_tokens": 40 } } }, "error": null } ] } ``` Requests that failed are not in `results`; they come back in a separate `errors_detail` array, each entry carrying its `custom_id` and the error. Both arrays are only returned once the batch is `completed`. Common statuses: * `validating` * `in_progress` * `finalizing` * `completed` * `failed` * `cancelling` * `cancelled` * `expired` ## Notes * All requests in a batch must use the same model. * Batch jobs are best for throughput, not low-latency interactive use. * Use `GET /v1/batches` to list recent batches and `POST /v1/batches/{batch_id}/cancel` to cancel one. * Results are matched by `custom_id` — the output order is not guaranteed. *** ## Batch statuses | Status | Meaning | | - | - | | `validating` | Submitted — file is being validated before processing starts | | `in_progress` | Processing — keep polling | | `finalizing` | Processing complete — output file is being assembled | | `completed` | All requests finished | | `failed` | Batch-level failure. The reason is in the batch's `errors` object; there is no `results` or `errors_detail` | | `cancelling` | Cancellation requested — winding down | | `cancelled` | Cancelled before completion | | `expired` | Batch exceeded the completion window and was not finished | ## Cancelling a batch ```bash theme={null} curl -X POST https://api.meshapi.ai/v1/batches/batch_01abc.../cancel \ -H "Authorization: Bearer rsk_YOUR_KEY" ``` Returns the updated batch object. The status transitions to `cancelling` and eventually `cancelled`. Partial results may still be available. ## Listing batches ```bash theme={null} curl https://api.meshapi.ai/v1/batches \ -H "Authorization: Bearer rsk_YOUR_KEY" ``` Supports cursor-based pagination via `after` (batch ID) and `limit` (1–100, default 20) query parameters. List rows are a summary: `id`, `status`, `model`, file IDs and timestamps. They don't carry `request_counts` or results; fetch a batch by ID for those. ## When to use batching * **Large-scale data processing** — summarizing, classifying, or translating thousands of documents * **Overnight jobs** — non-urgent workloads that can run during off-peak hours * **Throughput without client-side orchestration** — submit thousands of requests in one call. Batch requests are billed at the model's standard (on-demand) rates, not a discounted batch rate # Bring Your Own Keys (BYOK) Source: https://developers.meshapi.ai/docs/capabilities/byok Use your own upstream provider credentials instead of the shared system keys. By default, Mesh API routes inference through shared system credentials managed by us. With BYOK, you register your own provider API keys. Requests from your account then use your keys — giving you direct billing, quota control, negotiated rates, data-residency control, and access to provider-side features tied to your own account. BYOK is opt-in and optional. Running on Mesh's provider credentials is the right choice for most accounts. ## Supported providers | Provider | Registration endpoint | Credential type | | - | - | - | | AWS Bedrock | `POST /v1/provider-keys/aws` | Bedrock API key (region-locked) or IAM access key + secret | | Google Vertex AI | `POST /v1/provider-keys/vertex` | Service account JSON fields | | OpenAI | `POST /v1/provider-keys/openai` | API key | | Azure AI Foundry | `POST /v1/provider-keys/azure-foundry` | Project endpoint + API key | | Anthropic, Arcee, Cerebras, Cohere, DeepSeek, Fireworks, Moonshot, Parasail, Perplexity, Qwen (DashScope), Sarvam | `POST /v1/provider-keys/{provider}` | API key | There is no separate Azure OpenAI registration. One Azure AI Services key serves both hostnames, so register it as Azure AI Foundry. The pasted-key list is served by `GET /v1/provider-keys/supported`, which the dashboard's provider picker renders from. AWS Bedrock, Vertex AI, OpenAI, and Azure AI Foundry have a matching test endpoint. `POST /v1/provider-keys/test/{provider}` validates credentials without saving them, and `POST /v1/provider-keys/test/{pk_id}/{provider}` re-tests one you already registered (`{provider}` is `aws`, `vertex`, `openai`, or `azure-foundry`). A pasted key is validated against the provider when you save it: a rejected key returns `422` and nothing is stored. Credential checks are limited to 10 per minute per account. ## Platform fee BYOK traffic carries a platform fee of **5% of the request's cost at Mesh's list price**. The first **1,000,000** BYOK tokens each month are fee-free. The fee counts toward your spend caps. ## Adding a provider key ### Via Dashboard Open the [Dashboard](https://app.meshapi.ai) and select **BYOK** from the sidebar. The page lists each supported provider; select the one you want to configure. Inside the provider page, use the **Add** button to register a key. You can add multiple keys per provider, but only one can be active at a time. A new key is saved inactive, so enable the one you want to use. The `/v1/provider-keys` endpoints back this page. They take a signed-in dashboard session; an `rsk_` or `mak_` key gets `401`. Registering, rotating, and removing provider keys is always a dashboard action. The bodies under [Credential format by provider](#credential-format-by-provider) show what each form collects. Credentials are stored encrypted in GCP Secret Manager — plaintext is never written to the database. ## Using a provider key Once registered, requests for that provider automatically use your key based on the team associated with your API key. No changes to your API call are required — the routing is transparent. ## Fallback behavior By default, if your key fails due to an auth error or rate limit, the request transparently retries using the shared system credentials. To disable this, turn off **Allow fallback** on the key (`allow_fallback: false`): ```json theme={null} { "credential_type": "iam_credentials", "access_key_id": "AKIA...", "secret_access_key": "...", "region": "us-east-1", "allow_fallback": false, "team_id": "your-team-uuid" } ``` This is useful for compliance scenarios where requests must not leave your account. A response served by the fallback carries the `X-BYOK-Fallback-Triggered` header. Don't confuse it with `X-Mesh-Routing-Fallback`, which is about **providers and models** rather than credentials — see [Retry & Fallback](/docs/platform/retry-and-fallback). Both can appear on the same response. ## Limit a key to specific models By default a provider key serves every model Mesh routes to that provider. **Allowed models** narrows it to the models you name. Use it when the credential belongs to one contract, one budget, or an approved model set. Set it in the dashboard when you add or edit a key: **BYOK** → the provider → **Allowed models**. The list takes: * exact model IDs, such as `openai/gpt-4.1-mini` * brand wildcards, such as `openai/*`, which cover every model of that brand, including ones added later The list holds up to 200 entries. Each entry is checked against the model catalog when you save: an unknown ID, or a brand with no enabled model, returns `422` and nothing is saved. An empty list means every model. In the request body the field is `allowed_models`, on every create and update endpoint under [Key management](#key-management): ```json theme={null} { "api_key": "sk-...", "label": "Contract A", "allowed_models": ["openai/gpt-4.1-mini", "openai/gpt-4.1"], "allow_fallback": true, "team_id": "your-team-uuid" } ``` On an update, leaving `allowed_models` out keeps the current list. `null` or `[]` clears it. ### A model outside the list Your key is never used for a model outside its list. What happens instead depends on **Allow fallback**: | Allow fallback | Result | | - | - | | On (default) | Mesh's credentials serve the request as an ordinary request. It is billed to your Mesh balance at Mesh's price, the BYOK platform fee does not apply, and a zero balance returns `402`. This is not a fallback: on chat completions, Responses and embeddings the response carries `X-BYOK-Used: false` and `X-BYOK-Fallback-Triggered: false`. | | Off | `403` with `error.code` `byok_model_not_allowed`. Nothing is sent to the provider. | To serve an out-of-scope model on your own key, add the model or its brand wildcard to the list. A few things to know: * **The list never grants access.** Model policies on the API key, team, and organization are checked first, and a model they block is refused (`403 model_excluded`) whatever the key allows. Allowed models only decides whose credential serves a model that is already permitted. * **It applies everywhere the key is used.** That includes calls Mesh makes for you: the [Auto Router](/docs/capabilities/auto-routing)'s request classification, and the underlying models of a composite model (`is_composite` in the [model reference](/docs/reference/models)). With fallback off, a list that excludes one of a composite model's underlying models refuses requests for the composite model. * **Avoid changing the list while batch, video, or background jobs are running.** A running job is checked with the credential the new list picks. If that is a different provider account from the one that accepted the job, the check fails. ## Key management Endpoints the dashboard calls, for reference: | Action | Endpoint | | - | - | | Add key (AWS) | `POST /v1/provider-keys/aws` | | Add key (Vertex AI) | `POST /v1/provider-keys/vertex` | | Add key (OpenAI) | `POST /v1/provider-keys/openai` | | Add key (Azure AI Foundry) | `POST /v1/provider-keys/azure-foundry` | | Add key (pasted-key providers) | `POST /v1/provider-keys/{provider}` | | List pasted-key providers | `GET /v1/provider-keys/supported` | | List keys | `GET /v1/provider-keys` | | Key stats | `GET /v1/provider-keys/stats` | | Get key | `GET /v1/provider-keys/{id}` | | Enable / disable, relabel, change allowed models | `PATCH /v1/provider-keys/{id}` | | Update / rotate credential | `PATCH /v1/provider-keys/{aws,vertex,openai,azure-foundry}/{id}`, or `PATCH /v1/provider-keys/generic/{id}` for a pasted key | | Delete key | `DELETE /v1/provider-keys/{id}` | | Validate credentials (no save) | `POST /v1/provider-keys/test/{provider}` | | Test an existing key | `POST /v1/provider-keys/test/{id}/{provider}` | Deleting a provider key is a soft delete — the record is retained for billing history but excluded from future routing. Provider UUIDs are preserved for foreign key integrity. ## Credential format by provider ```json theme={null} { "credential_type": "iam_credentials", "access_key_id": "AKIAIOSFODNN7EXAMPLE", "secret_access_key": "wJalrXUtnFEMI...", "region": "us-east-1", "label": "My Bedrock key", "allow_fallback": true, "team_id": "your-team-uuid" } ``` Ensure the IAM user or role has `bedrock:InvokeModel`, `bedrock:InvokeModelWithResponseStream`, and `bedrock:ListFoundationModels` permissions on the models you intend to use. ```json theme={null} { "credential_type": "bedrock_api_key", "api_key": "your-bedrock-api-key", "region": "us-east-1", "label": "My Bedrock API key", "allow_fallback": true, "team_id": "your-team-uuid" } ``` Bedrock API keys are region-locked — the `region` must match the AWS region where the key was created and cannot be changed after creation. Provide the service account key fields directly (as downloaded from the GCP console or `gcloud iam service-accounts keys create`): ```json theme={null} { "type": "service_account", "project_id": "my-gcp-project", "private_key_id": "key-id", "private_key": "-----BEGIN RSA PRIVATE KEY-----\n...", "client_email": "my-sa@my-project.iam.gserviceaccount.com", "client_id": "123456789", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/...", "universe_domain": "googleapis.com", "region": "us-central1", "allow_fallback": true, "team_id": "your-team-uuid" } ``` The service account needs the `roles/aiplatform.user` role and `aiplatform.endpoints.predict` permission on your GCP project. Omit `region` or set it to `"global"` to allow requests to run in any available region. ```json theme={null} { "api_key": "sk-...", "label": "My OpenAI key", "allow_fallback": true, "team_id": "your-team-uuid" } ``` ```json theme={null} { "api_key": "your-azure-ai-services-key", "endpoint": "https://my-resource.services.ai.azure.com/api/projects/my-project", "label": "My Foundry key", "allow_fallback": true, "team_id": "your-team-uuid" } ``` `endpoint` is the full project endpoint URL; either the primary or secondary key works. ```json theme={null} { "api_key": "your-provider-api-key", "label": "My Anthropic key", "allow_fallback": true, "team_id": "your-team-uuid" } ``` The provider is the last path segment, for example `POST /v1/provider-keys/anthropic`. ## Troubleshooting Common BYOK failures and their causes: | Status | Cause | | - | - | | `401 Unauthorized` | Your provider API key is invalid, revoked, or malformed for that provider | | `403 Forbidden` | The key lacks permission for the requested model — e.g. an AWS IAM policy missing `bedrock:InvokeModel` | | `429 Too Many Requests` | You hit the rate limit on your own provider account | BYOK errors surface with the **provider's** HTTP status, not Mesh's. A `401` here means *your provider key* was rejected — not your `rsk_` key. For a symptom-by-symptom walkthrough, see [Troubleshooting → BYOK](/debug/byok). # Caching Source: https://developers.meshapi.ai/docs/capabilities/caching Cut cost and latency with the gateway response cache and provider-side prompt caching — how each one works, what it costs, and when it pays off. MeshAPI has **two independent caching layers**. They solve different problems and can be used together: | Layer | What it reuses | Where it lives | Who pays | | - | - | - | - | | **Gateway response cache** | The entire response to an identical request | MeshAPI (Redis) | Free by default — the upstream call never happens | | **Provider prompt caching** | The shared *prefix* of a prompt across different requests | The provider (Claude, Amazon Nova) | On Claude: reads cost 0.10× input, writes 1.25× — pays off as soon as a prefix is reused | The gateway cache is on by default and needs no code changes. Provider prompt caching is opt-in per request via `cache_control` markers. *** ## Gateway response cache When a request is byte-for-byte equivalent to one served recently, MeshAPI returns the stored response and never calls the provider. The result is a response in single-digit milliseconds at no token cost. The response cache covers `POST /v1/chat/completions` and `POST /v1/messages`. It also covers `POST /v1/responses` in two cases: * the request sets `store: false`; * the model has no native Responses API, so MeshAPI translates the request. A stored Responses request on a model with a native Responses API is never cached; `store: true` is the default. A Responses request is matched on its whole body, apart from `stream`, `user` and `service_tier`, rather than on the fields below. A hit replays the answer in the Responses format. ### What counts as "identical" The cache key is a SHA-256 hash over exactly these fields: * `model` * the resolved upstream `provider` * `messages` * `max_tokens` * `stop` * `response_format` * `service_tier`, when the request selected one — see [Service Tiers](/docs/capabilities/service-tiers) * each of `temperature`, `seed`, `top_p`, `frequency_penalty`, `presence_penalty`, `reasoning_effort` and `reasoning`, when the request sets it (unset is its own value, not `0`) * the parameters the serving model could not accept and Mesh withheld, if any Anything not in that list does **not** affect the cache key. Two things follow that surprise people: * **`stream` is not part of the key.** A streaming request can be served from a response originally stored by a non-streaming one; MeshAPI replays it as normal SSE chunks. Your streaming client sees no difference. * **`user` is not part of the key.** Passing a different `user` value does not give you a different cache entry. ### When caching applies A request is eligible only if **all** of these hold: | Condition | Why | | - | - | | `temperature` is `0` or omitted | Any non-zero temperature means non-deterministic output, which must not be reused. An omitted temperature is still cached, but under its own entry, separate from `temperature: 0`, because the provider sampled it at its own default (usually `1.0`). | | No `tools` in the request | A tool call depends on live state; replaying one would be wrong. | | Not an image-generation model | Image output is not stored in the response cache. | | No memory injected | A request whose `x-mem-id` memories were injected is personalized to one user. See [Memory](/docs/capabilities/memory). An `x-mem-id` that injects nothing doesn't bypass the cache. | | Response is under 64 KB serialized | Larger responses are served normally but never written to the cache. | The temperature rule is the one that catches people out. If you send `"temperature": 0.7`, you get **no caching at all**, no matter how many times you repeat the identical prompt. Deterministic workloads (classification, extraction, structured output) should send `temperature: 0` explicitly. ### Detecting a cache hit A response served from the cache carries: ```http theme={null} X-Cache: HIT ``` There is no `X-Cache: MISS` header. The header is **present only on a hit** — treat its absence as a miss rather than checking for a `MISS` value. The header is the reliable per-request signal: count `X-Cache: HIT` responses to measure your hit rate. The per-request [Usage API](/docs/reference/usage-api) events don't carry the hit marker, so you can't filter on it there. ### Turning it off Caching is enabled by default. You can opt out per request in two ways: ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -H "X-Mesh-Cache: no-store" \ -d '{ "model": "openai/gpt-4o-mini", "messages": [{ "role": "user", "content": "What time is it in Tokyo?" }] }' ``` ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o-mini", "messages": [{ "role": "user", "content": "What time is it in Tokyo?" }], "cache": false }' ``` Both skip the cache read *and* the cache write for that request. `"cache": true` does **not** force caching on. The flag is deliberately asymmetric: `false` is an unconditional opt-out, but `true` only means "cache if otherwise permitted" — it still respects your key's settings and every eligibility rule above. There is no way to cache a `temperature: 0.9` request. To disable caching for every request on a key, set it on the key itself rather than per request — see the [Account Configuration Checklist](/docs/getting-started/account-checklist). ### Cache isolation By default each account gets its **own** cache namespace: your responses are never served to another tenant, even for an identical prompt. Accounts may opt into a shared namespace, where entries are keyed by content hash alone and hit rates are higher because the cache is warmed by all participants. Shared caching is off unless you ask for it. ### Freshness Entries live for **24 hours**. There is no explicit invalidation API — if you need a guaranteed-fresh answer, send `X-Mesh-Cache: no-store`. *** ## Provider prompt caching Provider prompt caching is a different mechanism: instead of reusing a whole response, the provider stores the processed **prefix** of your prompt and charges less for it on subsequent requests. It pays off when you send a large, stable block — a long system prompt, a document, a few-shot set — followed by a small varying question. Unlike the gateway cache, this one still calls the provider and still generates a fresh completion. It reduces the cost of the *input* tokens, not the call. This one is **not** free money. Reading a cached prefix costs 10% of the input rate, but *creating* it costs 125% — so a prefix that is written and never read costs you more than not caching at all. See [What it costs](#what-it-costs) before turning it on across a workload. ### Marking a prefix Add a `cache_control` marker to the content you want cached. MeshAPI forwards it to providers that support server-side prompt caching. ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "anthropic/claude-sonnet-5", "messages": [ { "role": "system", "content": [ { "type": "text", "text": "", "cache_control": { "type": "ephemeral" } } ] }, { "role": "user", "content": "Summarize section 4." } ] }' ``` Each marker defines a breakpoint that caches **everything before it**. Put markers at the end of the stable region, immediately before the part that changes between requests. ### Where markers are honoured Whether `cache_control` reaches the provider depends on **which provider serves the model**, not on the model name alone. The same model can behave differently depending on how it is routed. | Serving provider | What happens to `cache_control` | | - | - | | Anthropic, Google, Meta, Mistral and other routed models | Forwarded upstream. Applied on models that support caching and dropped on those that don't. | | Amazon Bedrock | Converted to a native cache point — but only for **Claude** and **Amazon Nova** models. Dropped for all other Bedrock models. | | Direct adapters (OpenAI, Together, DeepInfra, Qwen, Vertex) | **Stripped before the request is sent.** These APIs reject or ignore the field. | OpenAI's direct API performs prompt caching **automatically** on long prompts — you do not mark it, and you cannot control it from here. Markers are removed on that route not because caching is unavailable, but because the provider manages it itself. An unsupported marker is **silently removed** — it never errors. Your request succeeds and returns a normal completion, and nothing in the response indicates the marker was dropped. Never assume a marker took effect: confirm it against `cached_tokens` in the response usage block. On Bedrock a request may carry at most **4** cache breakpoints. If you send more, MeshAPI keeps the last 4 — later breakpoints cover longer prefixes, so keeping them preserves the most caching value. You can see which provider served any request in your [usage records](/docs/reference/usage-api). ### Reading the result Every response tells you exactly what the cache did, under `usage.prompt_tokens_details`: ```json theme={null} "usage": { "prompt_tokens": 6809, "completion_tokens": 40, "total_tokens": 6849, "prompt_tokens_details": { "cached_tokens": 6797, // read from the cache — charged at 10% "cache_write_tokens": 0, // written to the cache — charged at 125% "cache_write_1h_tokens": 0, // the part of cache_write_tokens written with a 1h TTL "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 }, "byok_used": false, "fallback_triggered": false } ``` Three states, and you should be able to tell them apart: | What you see | What happened | | - | - | | `cache_write_tokens` > 0 | Cold call. The prefix was just stored; you paid the premium. | | `cached_tokens` > 0 | Hit. That many tokens were billed at the reduced rate. | | `cached_tokens` and `cache_write_tokens` both 0 | **Nothing was cached at all** — no marker, or one that did not take effect. | `prompt_tokens` **includes** both numbers, so the tokens processed fresh at the normal rate are `prompt_tokens - cached_tokens - cache_write_tokens`. Every key above is present on every chat completion, whichever provider served it, and is `0` when unused — you can index them directly. Two limits on what a `0` means: * `cache_write_1h_tokens` is filled only where the serving provider reports the 1h split. A route that doesn't report it reads `0` even after a 1h write. * A route that does not report cache writes at all returns `cache_write_tokens: 0`. Which routes report which counts, and the usage shapes on `/v1/responses` and `/v1/messages`, are in the [Usage Object](/docs/reference/usage-object#cache-reporting-by-serving-provider) reference. `total_tokens` is always `prompt_tokens + completion_tokens`. **Deprecated:** the top-level `usage.cache_creation_input_tokens` that Claude responses carry duplicates `prompt_tokens_details.cache_write_tokens`. It still appears for now; read `cache_write_tokens` instead. On a streaming request the usage block arrives on the **final chunk**. You do not need to ask for it — MeshAPI requests usage from the provider on every stream, so the cache breakdown is there either way. The per-request [Usage API](/docs/reference/usage-api) (`/v1/usage`, `/v1/usage/events`) returns no cache fields: not `cached_tokens`, and not the marker that distinguishes a gateway hit. Aggregates are available from `GET /v1/analytics/usage/summary` with `metric=cached_tokens` and `metric=cache_hit_rate` (an API key reads only its own traffic). For a per-request breakdown, record `prompt_tokens_details` from each response yourself. ### What it costs Cached tokens are not billed at the normal input rate. Every **Claude** model on MeshAPI uses the same two multipliers: | Token type | Rate | On `anthropic/claude-sonnet-5` (\$2.00 / 1M input) | | - | - | - | | Cache **read** | 0.10× input | \$0.20 / 1M | | Cache **write** | 1.25× input | \$2.50 / 1M | | Ordinary input | 1× | \$2.00 / 1M | Measured end to end on `anthropic/claude-sonnet-5` with a 6,800-token system block, same prompt each time: | Call | Result | Cost at list | vs uncached | | - | - | - | - | | First, with marker | 6,797 written | \$0.01742 | 1.24× | | Second, with marker | 6,797 read | \$0.00178 | **0.13×** | | Same prompt, no marker | nothing cached | \$0.01402 | 1.00× | So a hit costs about an eighth of the uncached call, and the write premium is repaid inside the first hit — two calls sharing a prefix are already \~31% cheaper than two uncached ones. **What decides whether it pays is reuse, not prompt share.** A segment costs 1.25× once and 0.10× on every later read, against 1.00× each time uncached — so it is ahead the first time it is read (1.25 + 0.10 \< 2.00), however small a fraction of the prompt it is. A 2,000-token prefix on a 50,000-token request still pays, provided it is reused. The way to lose money is a write that is **never read**. Each unread write costs 25% more than not caching at all. That happens when the marked region changes between calls, when the gap between calls exceeds the TTL, or when a marker sits after the part that varies. If you cannot say which later request will reuse the prefix, do not mark it. ### Holding a prefix for an hour The default entry expires after \~5 minutes. If your calls are further apart than that, add a TTL and the prefix survives an hour instead: ```json theme={null} "cache_control": { "type": "ephemeral", "ttl": "1h" } ``` The longer hold costs more to create — **2× input** instead of 1.25× — so it only pays when it converts writes into reads. Rule of thumb: if the gap between calls sharing a prefix is reliably under 5 minutes, stay on the default; if it's typically longer, the 1h write is cheaper than paying a 5m write every time. | | 5m (default) | 1h | | - | - | - | | Write | 1.25× input | 2× input | | Read | 0.10× input | 0.10× input | | On `claude-sonnet-5` | \$2.50 / 1M write | \$4.00 / 1M write | Writes made under a 1h TTL are reported separately as `cache_write_1h_tokens` in `prompt_tokens_details`, so you can tell the two apart on the bill. On Bedrock the TTL applies to **Claude** models only, and it is all-or-nothing per request: MeshAPI reports the 1h rate only when *every* breakpoint in the request carries `ttl: "1h"`. Mix 5m and 1h markers in one call and the whole request bills at the 5m rate. A model whose price row has no 1h rate configured also bills 1h writes at the 5m rate. ### Multi-turn: move the breakpoint forward The biggest win is a conversation, not a single call. Each turn resends everything before it, so with one marker pinning the stable head and a second rolling onto the newest message, every turn reads the whole previous turn and writes only the delta. ```json theme={null} "messages": [ { "role": "system", "content": [ { "type": "text", "text": "", "cache_control": { "type": "ephemeral" } } ]}, { "role": "user", "content": "first question" }, { "role": "assistant", "content": "..." }, { "role": "user", "content": "newest question", "cache_control": { "type": "ephemeral" } } ] ``` Move the second marker to the newest message on every turn rather than adding one per turn: the limit is 4. MeshAPI trims to the last 4 for you on Bedrock and the direct Anthropic route; OpenRouter forwards markers verbatim, so a 5th there is an upstream 400; every other route strips them entirely (see the table above). Measured over four turns with the model's real replies fed back, turns 2–4 each read the entire previous prefix and wrote a 45–78 token tail, holding at **0.14×** the uncached cost while the conversation grew. ### When a marker doesn't take effect A marker that does nothing is silent — the request succeeds and returns a normal completion. If `cached_tokens` and `cache_write_tokens` both come back `0`, work down this list: Providers refuse to store a prefix below a minimum, and it is model-dependent: **1,024 tokens** on Sonnet and Opus, **2,048** on Haiku. Under it you get no cache entry and no error. MeshAPI does not validate this for you. Tool definitions sit **ahead of** the system block in the cached prefix, so editing, adding or removing a tool invalidates everything after it. Measured: the same system block with one extra tool schema returned `cached_tokens: 0` and re-wrote all 7,153 tokens. Keep `tools` byte-stable across the calls meant to share a prefix — including the order of the array. A default `ephemeral` entry lives about **5 minutes**, refreshed each time it is read. A loop that idles longer between turns pays the write again and again and never collects the discount — see [Holding a prefix for an hour](#holding-a-prefix-for-an-hour). Dropping the oldest messages to stay under a limit changes the *start* of the prompt, which is exactly what the cache keys on — so nothing after it can be reused. Summarize into the system block instead of dropping the head — otherwise every trim turns that turn's write into one that is never read. See [Where markers are honoured](#where-markers-are-honoured) — direct OpenAI, Together, DeepInfra, Qwen and Vertex strip the field, and Bedrock applies it only to Claude and Nova. Check which provider actually served the request in your [usage records](/docs/reference/usage-api). A request that fails upstream does **not** warm the cache, so a rejected call costs you nothing in cache writes — the next valid call is a normal cold write. *** ## Choosing between them * **Repeating identical requests** (dashboards, evals, retries, deterministic pipelines) → the gateway cache does this for free, automatically. Just keep `temperature` at `0`. * **Varying questions over a large fixed context** (document Q\&A, long system prompts, few-shot, agent loops) → the gateway cache never hits, because the messages differ every time. Use provider prompt caching: the prefix is reused on every question, which is exactly when it pays. * **Prompts with nothing stable in them** (each call unique, or calls further apart than the TTL) → neither. A marker here is written and never read, which costs 25% more than not caching. * **Both** are valid together: a repeated request hits the gateway cache first and never reaches the provider at all. ## Related * [Memory](/docs/capabilities/memory) — why memory-attached requests bypass the response cache * [Usage & Monitoring API](/docs/reference/usage-api) — measuring hit rate and savings * [Usage Object](/docs/reference/usage-object) — every `usage` field, and which cache counts each provider reports * [Pricing](/docs/getting-started/pricing) — how cached tokens are billed # Claude Code Source: https://developers.meshapi.ai/docs/capabilities/claude-code Route Claude Code through Mesh to reach every model in the catalog, with your spend caps, rate limits and usage logging applied. Claude Code talks the [Anthropic Messages API](/docs/capabilities/messages-api) to whatever `ANTHROPIC_BASE_URL` points at. Point it at Mesh and every request runs through the same gateway as the rest of your traffic — spend caps, per-key rate limits, usage logging and retry/fallback all apply, with no change to how you use the tool. Because `/v1/messages` is **not restricted to Anthropic models**, this also lets Claude Code run against any model in the Mesh catalog. ## Configuration Add this to `~/.claude/settings.json` (or a project's `.claude/settings.json`): ```json theme={null} { "env": { "ANTHROPIC_BASE_URL": "https://api.meshapi.ai", "ANTHROPIC_AUTH_TOKEN": "rsk_YOUR_KEY", "ANTHROPIC_DEFAULT_OPUS_MODEL": "anthropic/claude-opus-4.8", "ANTHROPIC_DEFAULT_SONNET_MODEL": "anthropic/claude-sonnet-4.6", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "anthropic/claude-haiku-4.5" } } ``` All three model variables matter. Claude Code picks a model per task, and any variable you leave unset falls through to a Claude Code default id that Mesh may not serve (see §2 below). **Mesh's canonical ids use dots.** Mesh serves `anthropic/claude-sonnet-4.6`. Anthropic's native hyphenated form (`claude-sonnet-4-6` or `anthropic/claude-sonnet-4-6`) is mapped to the dotted id when exactly one catalog model matches it, but suffixes Claude Code adds, such as `[1m]`, are not. Copy the ids exactly as written above, or from [Models & Pricing](https://meshapi.ai/models). ## The three things that catch people out ### 1. If you are signed in to Claude Code, `ANTHROPIC_AUTH_TOKEN` is ignored This is the most common failure, and the error does not hint at the cause: ``` Failed to authenticate. API Error: 401 Invalid or missing API key. ``` A signed-in Claude Code prefers its stored credential over the environment variable, so it sends that credential to Mesh — which correctly rejects it as not being a Mesh key. Use a separate config directory so the CLI has no stored login to prefer: ```bash theme={null} CLAUDE_CONFIG_DIR=~/.claude-mesh claude ``` Wrap it in a shell alias if you switch between the two regularly: ```bash theme={null} alias claude-mesh='CLAUDE_CONFIG_DIR=~/.claude-mesh claude' ``` Signing out of Claude Code also works, but costs you the direct-Anthropic setup. A second config directory keeps both. ### 2. `ANTHROPIC_DEFAULT_OPUS_MODEL` is not optional Claude Code's default model is Opus. Set only Sonnet and Haiku and the first request fails: ``` There's an issue with the selected model (claude-opus-4-8[1m]). It may not exist or you may not have access to it. ``` Note the `[1m]` suffix Claude Code appends for its long-context variant — another reason to map the variable explicitly rather than relying on a default. ### 3. Haiku carries real traffic `ANTHROPIC_DEFAULT_HAIKU_MODEL` is easy to skip because you never select Haiku yourself. Claude Code uses it for background work — conversation titles, summaries — so leaving it unset sends a steady trickle of requests to an id Mesh does not serve. In a short verification session, roughly a third of the requests were Haiku. ## Verify it works ```bash theme={null} CLAUDE_CONFIG_DIR=~/.claude-mesh claude -p 'Say OK' ``` Then confirm the traffic actually reached Mesh — this is the check that distinguishes "configured" from "working", since a misconfigured client can still answer from a direct Anthropic connection: Open [Usage](https://app.meshapi.ai/usage). Requests from Claude Code appear under the key you configured, with the model ids you mapped. ```bash theme={null} curl https://api.meshapi.ai/v1/usage/events \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"org_id": "YOUR_ORG_ID", "limit": 5}' ``` `org_id` is required by the request schema; with an API key, results are scoped to that key regardless. Look for recent rows whose `model` is one of the ids you mapped. See [Usage API](/docs/reference/usage-api). ## Model discovery (optional) Claude Code can populate its `/model` picker from the Mesh catalog instead of its built-in list: ```json theme={null} { "env": { "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1" } } ``` Two caveats: * Discovery calls `GET /v1/models`, which accepts **`Authorization: Bearer` only** — not `x-api-key`. With `ANTHROPIC_AUTH_TOKEN` (as configured above) this is already correct; if you switch to `ANTHROPIC_API_KEY`, inference keeps working but discovery returns 401. * The catalog is large — the response covers the full model list and is fetched on start-up. ## What runs through the gateway Everything, which is the point of routing Claude Code this way: | | | | - | - | | **Spend caps** | A Claude Code session cannot exceed the key's cap. BYOK-served requests count only their platform fee toward it | | **Rate limits** | Per-key RPM/RPD apply | | **Usage logging** | Every request is logged in your usage history | | **Retry & fallback** | [Resilient routing](/docs/platform/retry-and-fallback) applies | | **BYOK** | Your own provider keys are used when configured | ## Troubleshooting | Symptom | Cause | | - | - | | `401 Invalid or missing API key` | Signed-in CLI overriding the token — use `CLAUDE_CONFIG_DIR` (§1) | | `issue with the selected model (claude-opus-4-8[1m])` | `ANTHROPIC_DEFAULT_OPUS_MODEL` unset (§2) | | `404 Model ... is not supported` | An ID Mesh can't map. Check it against [Models & Pricing](https://meshapi.ai/models) (`anthropic/claude-sonnet-4.6`) | | Works, but no rows in Usage | Requests are not reaching Mesh — re-check `ANTHROPIC_BASE_URL` | | `402` mid-session | Key's spend cap reached, or the account balance is exhausted | ## Related * [Messages API](/docs/capabilities/messages-api) — the endpoint Claude Code uses * [Models & Pricing](https://meshapi.ai/models) — exact ids for the model variables * [Rate Limits](/docs/getting-started/rate-limits) — what applies to a session * [Retry & Fallback](/docs/platform/retry-and-fallback) — behaviour on provider failure # Embeddings Source: https://developers.meshapi.ai/docs/capabilities/embeddings Create dense vector embeddings with the Mesh API and the official SDKs. The Embeddings API turns text into vectors you can use for semantic search, retrieval, clustering, and ranking. ```http theme={null} POST https://api.meshapi.ai/v1/embeddings Authorization: Bearer rsk_ ``` ## Quick start ```bash theme={null} curl https://api.meshapi.ai/v1/embeddings \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "openai/text-embedding-3-small", "input": "The quick brown fox jumps over the lazy dog." }' ``` ```ts theme={null} import { MeshAPI } from "meshapi-node-sdk"; const client = new MeshAPI({ baseUrl: "https://api.meshapi.ai", token: process.env.MESH_API_KEY!, }); const response = await client.embeddings.create({ model: "openai/text-embedding-3-small", input: "The quick brown fox jumps over the lazy dog.", }); console.log(response.data[0]?.embedding); ``` ```python theme={null} from meshapi import EmbeddingsParams, MeshAPI client = MeshAPI( base_url="https://api.meshapi.ai", token="rsk_...", ) response = client.embeddings.create( EmbeddingsParams( model="openai/text-embedding-3-small", input="The quick brown fox jumps over the lazy dog.", ) ) print(response.data[0].embedding) ``` ## Request fields | Field | Type | Notes | | - | - | - | | `model` | string | Embedding model ID. Required unless your key has a default model. `"auto"` is not accepted (`400`). | | `input` | string \| string\[] \| int\[] \| int\[]\[] \| object\[] | Text or tokenized input. BytePlus multimodal embedding models also take a list of multimodal input objects. | | `dimensions` | integer | Optional output size for models that support truncation. | | `encoding_format` | `"float"` \| `"base64"` | Output format. | | `input_type` | string | Optional hint for asymmetric embedding models. | | `provider` | string \| object | Optional provider routing preferences. | | `instructions` | string | Optional task instructions for models that support them (e.g. BytePlus). | | `sparse_embedding` | object | Optional, BytePlus text embeddings. `{"type": "enabled"}` returns dense and sparse vectors; `{"type": "disabled"}` returns dense only. | | `safety_identifier` | string | Optional end-user identifier for abuse monitoring (max 256 characters). | | `user` | string | Deprecated spelling of `safety_identifier`. Still accepted. | | `tags` | object | Optional labels for the request, recorded on its usage row and never sent upstream. | ## Batch input in a single request You can embed multiple strings in one API call: ```bash theme={null} curl https://api.meshapi.ai/v1/embeddings \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "openai/text-embedding-3-small", "input": [ "first sentence", "second sentence", "third sentence" ] }' ``` The returned `data` array matches the order of your input array. ## Response shape ```json theme={null} { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [-0.0123, 0.0456] } ], "model": "openai/text-embedding-3-small", "usage": { "prompt_tokens": 12, "total_tokens": 12 } } ``` ## Choosing a model Use `GET /v1/models` to inspect available embedding models and their pricing. For embeddings, the most useful fields are: * `model_type` * `context_length` * `pricing` * `is_free` ## SDK coverage The official SDKs all expose first-class embeddings resources: * Node: `client.embeddings.create(...)` * Python: `client.embeddings.create(...)` * Go: `client.Embeddings.Create(...)` * Java: `client.embeddings().create(...)` ## Common errors | HTTP | Meaning | | - | - | | `401` | Missing or invalid API key | | `400` | `model_capability_not_supported`: the model isn't an embedding model. `unprocessable_entity`: you sent `model: "auto"` | | `402` | Balance or spend limit issue | | `422` | Invalid request body | | `429` | Rate limit exceeded | *** ## Available models | Model | Dimensions | Best for | | - | - | - | | `amazon/titan-embed-text-v2` | 1024 | General-purpose English text | | `openai/text-embedding-3-small` | 1536 | Fast, cost-efficient | | `openai/text-embedding-3-large` | 3072 | Highest accuracy | | `openai/text-embedding-ada-002` | 1536 | Legacy OpenAI compatibility | Check `GET /v1/models` with the `type=embedding` filter to see the full live list of enabled embedding models. ## RAG with file uploads For retrieval-augmented generation over your own documents, use the [Files & RAG](/docs/capabilities/rag) workflow — upload files, trigger embedding, and search with `POST /v1/files/search`. This is more scalable than calling the embeddings endpoint directly for large document corpora. # Evaluate Source: https://developers.meshapi.ai/docs/capabilities/evaluate Ask a model a typed question and get a calibrated probability back instead of prose to parse — choices, scores, and likelihoods from POST /v1/evaluate. `POST /v1/evaluate` answers structured questions about a piece of application state. Instead of prose you have to parse, each answer comes back typed — a chosen option with a probability for every option, a score against a scale you define, or a single probability. ```http theme={null} POST https://api.meshapi.ai/v1/evaluate Authorization: Bearer rsk_ ``` Use it for the decisions an application makes over and over: which queue a ticket belongs in, how urgent a message is, whether something needs a human. One request can ask several questions about the same state. This endpoint is **beta**: it depends on an upstream path Mesh API does not control, so the contract may change. It is also available as the `evaluate` tool on the [Mesh MCP server](/agent/mcp). An ordinary [API key](/docs/getting-started/api-keys) calls this endpoint. If you have restricted a key to a specific set of scopes, that set has to include **`evaluate:write`** — a key with no scope restrictions already has it. ## How it differs from structured output [Structured output](/docs/capabilities/structured-output) makes a chat model return JSON in a shape you specify. It is the right tool when you want the model to *produce content* in a fixed format. Evaluate is for *decisions*. You give it the options, and it returns how likely each one is — a calibrated number you can threshold, route on, or log. You never write a prompt asking for JSON, and there is nothing to parse or repair. | | Structured output | Evaluate | | - | - | - | | You supply | A JSON schema | The questions and their options | | You get back | JSON as a string in `message.content` | Typed answers, already parsed | | Confidence | Not available | A probability per option | | Failure mode | Model ignores the schema, returns prose | Not possible — the shape is fixed | ## Models | Model | Price | Context | | - | - | - | | `typesafe/jev-1.13` | **\$0.042 / 1M input tokens**, output free | 32,000 tokens | ## Question types Every question declares a `type`, and that decides which `criteria` shape is legal. | `type` | `criteria` | Answer | | - | - | - | | `choice` | An object of `{your_key: description}` | The winning key, plus a probability for every key | | `score` | An ordered array, lowest first | A number on that scale, with the scale echoed back as a legend | | `noul` | Not allowed | A single probability between 0 and 1 | ## Making a request `state` is whatever your application looks like — a string, an object, or an array. Every question is answered against that same state. ```bash theme={null} curl https://api.meshapi.ai/v1/evaluate \ -H "Authorization: Bearer rsk_" \ -H "Content-Type: application/json" \ -d '{ "model": "typesafe/jev-1.13", "state": { "subject": "Refund for order 4821 never arrived", "plan": "pro" }, "questions": { "department": { "type": "choice", "instructions": "Which team should handle this ticket?", "criteria": { "billing": "Payments, refunds and invoices", "technical": "Bugs, outages and integration problems" } }, "urgency": { "type": "score", "instructions": "How urgently does this need a reply?", "criteria": ["Can wait a week", "Within a day", "Immediately"] }, "needs_human": { "type": "noul", "instructions": "Probability that this needs a human reply." } } }' ``` ```python theme={null} import httpx resp = httpx.post( "https://api.meshapi.ai/v1/evaluate", headers={"Authorization": "Bearer rsk_"}, json={ "model": "typesafe/jev-1.13", "state": { "subject": "Refund for order 4821 never arrived", "plan": "pro", }, "questions": { "department": { "type": "choice", "instructions": "Which team should handle this ticket?", "criteria": { "billing": "Payments, refunds and invoices", "technical": "Bugs, outages and integration problems", }, }, "urgency": { "type": "score", "instructions": "How urgently does this need a reply?", "criteria": ["Can wait a week", "Within a day", "Immediately"], }, "needs_human": { "type": "noul", "instructions": "Probability that this needs a human reply.", }, }, }, ) answers = resp.json()["answers"] if answers["needs_human"]["noul"] > 0.7: escalate(to=answers["department"]["choice"]) ``` You name the questions yourself, and those names come back as the keys of `answers`. ## The response ```json theme={null} { "id": "eval_2f9c4a1b8d3e5f7a6c0b9d2e", "model": "typesafe/jev-1.13", "answers": { "department": { "type": "choice", "choice": "billing", "probabilities": { "billing": 1, "technical": 0 }, "confidence": 1 }, "urgency": { "type": "score", "score": 1.3, "legend": { "0": "Can wait a week", "1": "Within a day", "2": "Immediately" }, "probabilities": { "0": 0.01, "1": 0.68, "2": 0.31 }, "confidence": 0.53 }, "needs_human": { "type": "noul", "noul": 0.8 } }, "usage": { "input_tokens": 396, "output_tokens": 64, "total_tokens": 460 } } ``` **`noul` is a probability, not a true/false.** It is a number between 0 and 1, and `0.8` means "probably" — not "yes". Compare it against a threshold you choose; treating it as a boolean makes every non-zero answer true. **A `score` legend is keyed by position in the array you sent.** `"1"` means the second entry of `criteria`. Reordering that array therefore changes what a score means, silently and for every past comparison — treat the order as part of your schema. ## Request rules Anything the endpoint cannot make sense of is rejected with a `422` before the model is called, so you are never billed for a question that could not be answered. | Field | Rules | | - | - | | `model` | Required. An evaluation model id — see [Models](#models). | | `state` | Required. A string, object, or array. | | `questions` | Required, at least one. Each key is your own name for the question: letters, digits, `_` and `-`, up to 64 characters. | | `instructions` | Required on every question, and cannot be blank. | | `criteria` | At least **two** entries on `choice` and `score`. No blank keys, descriptions, or scale points. Not allowed on `noul`. | Two options is the floor because a single-option choice is not a decision — the model can only pick the one you gave it, and you would pay input tokens for an answer that was never in doubt. Blank criteria are refused for the same reason: a scale with no labels produces a number with no meaning. Fields this endpoint does not define are **rejected rather than ignored**, so a misspelled `criteria` fails loudly instead of quietly changing the question. ## Attribution and limits Two optional fields let you attribute the request in your own usage reports. Neither is ever forwarded to the model provider. | Field | What it does | | - | - | | `tags` | Your own labels, as an object of up to 16 string pairs. Filter and group spend by them in usage reports — useful when several apps share one key. | | `safety_identifier` | Your own id for the end user the request was made for, so spend can be attributed per user. | [Rate limits](/docs/getting-started/rate-limits), spend caps, and credit balance apply exactly as they do on `/v1/chat/completions`. There is **no streaming** on this endpoint: an evaluation returns one small JSON body, so there is nothing to stream. ## Errors | Status | Meaning | | - | - | | `400` | The model cannot serve evaluations (`model_capability_not_supported`), or your request is larger than the model's context window | | `404` | The model does not exist (`model_not_found`) | | `401` / `403` | Missing or invalid key; suspended key, or one without `evaluate:write` | | `402` | Not enough credits | | `422` | The request body failed validation — see [Request rules](#request-rules) | | `429` | Rate limit reached | | `503` / `504` | The provider returned an error or did not respond in time | # Image Generation Source: https://developers.meshapi.ai/docs/capabilities/image-generation Generate and edit images using frontier vision models via a single API. Generate images from text prompts — or edit existing images — using models from OpenAI, Google Vertex AI, and more. The API is compatible with the OpenAI images endpoint, so existing integrations work without modification. ## Generate an image ```bash theme={null} curl https://api.meshapi.ai/v1/images/generations \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-image-1-mini", "prompt": "A children drawing of a veterinarian and a baby otter", "response_format": "url" }' ``` ```python theme={null} from openai import OpenAI client = OpenAI(api_key="rsk_...", base_url="https://api.meshapi.ai/v1") response = client.images.generate( model="openai/gpt-image-1-mini", prompt="A children drawing of a veterinarian and a baby otter", response_format="url", ) print(response.data[0].url) ``` **Response:** ```json theme={null} { "created": 1717200000, "data": [ { "url": "data:image/png;base64,iVBORw0KGgo..." } ] } ``` GPT image and Google Vertex AI models return image bytes, not a hosted link, so with `response_format: "url"` the `url` field holds a `data:` URI. Other providers may return an `https://` link. ## Request fields Not all parameters are supported by every model. Support varies by provider — unsupported fields are silently ignored or return an error depending on the model. Check the model's documentation or test with your target model before relying on a specific parameter. | Field | Type | Description | | - | - | - | | `model` | string | Image model ID (required) | | `prompt` | string | Text description of the image to generate | | `n` | integer | Number of images to generate, 1–10 (default: 1) | | `size` | string | Image dimensions, e.g. `"1024x1024"`, `"1536x1024"`, or `"auto"` (default) | | `quality` | string | `"auto"` (default); `"low"` / `"medium"` / `"high"` for GPT image models (`openai/gpt-image-2.5-*` also take `"xhigh"` / `"max"`); `"1K"` / `"2K"` / `"4K"` for Gemini 3 image models, which also map `low` / `medium` / `high` to those sizes | | `aspect_ratio` | string | e.g. `"16:9"` or `"1:1"`. Honoured by xAI Grok Imagine and Gemini 3 image models. Derived from `size` when omitted | | `resolution` | string | `"1k"` or `"2k"`, xAI Grok Imagine only. Derived from `size` when omitted | | `image` | string or string\[] | Reference image(s) for image-conditioned generation, as base64, `data:` URI or URL. Only for models with `supports_image_reference: true` on `GET /v1/models` (BytePlus Seedream, Gemini 3 image, Grok Imagine); other models return `422`. Gemini does not accept `http(s)` URLs | | `response_format` | string | `"url"` (default) or `"b64_json"` | | `output_format` | string | Output encoding: `"png"`, `"jpeg"`, or `"webp"` on GPT image models; BytePlus Seedream takes `png` / `jpeg` | | `output_compression` | integer | Compression level 0–100 for `jpeg`/`webp` output — GPT image models only | | `background` | string | Transparency: `"transparent"`, `"opaque"`, or `"auto"` — GPT image models only | | `moderation` | string | Content-moderation level: `"low"` or `"auto"` — GPT image models only | | `partial_images` | integer | 0–3, forwarded to GPT image models. Mesh does not stream partial images from them. See [Streaming](#streaming) | | `seed`, `guidance_scale`, `watermark`, `optimize_prompt` | — | BytePlus Seedream only: seed (-1 to 2147483647), prompt adherence 1–10, "AI generated" watermark, prompt rewriting on/off | | `stream` | boolean | Return a `text/event-stream`. See [Streaming](#streaming) | | `tags` | object | Your own labels, as an object of up to 16 string pairs. Filter and group spend by them in usage reports | | `safety_identifier` | string | A stable, anonymised id for the end user the request was made for (a hash, not an email), so spend can be attributed per user | ## Edit an image Transform an existing image with `POST /v1/images/edits`: edit it with a prompt (optionally guided by a mask), remove its background, upscale, outpaint, inpaint, mix or reframe it. Send either `multipart/form-data` (file uploads, as below) or `application/json` with each image as a `data:image/;base64,...` string (or a `{"url": "data:..."}` object). Remote `http(s)` image URLs are rejected with `422`, because Mesh does not fetch images. ```bash theme={null} curl https://api.meshapi.ai/v1/images/edits \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -F "model=openai/gpt-image-1" \ -F "operation=edit" \ -F "prompt=Add a party hat on the cat" \ -F "image=@cat.png" ``` ```python theme={null} from openai import OpenAI client = OpenAI(api_key="rsk_...", base_url="https://api.meshapi.ai/v1") response = client.images.edit( model="openai/gpt-image-1", prompt="Add a party hat on the cat", image=open("cat.png", "rb"), ) print(response.data[0].url) ``` **Response:** ```json theme={null} { "created": 1717200000, "data": [ { "url": "data:image/png;base64,..." } ] } ``` GPT image and Vertex AI models return the result as a base64 `data:` URI in the `url` field. Pass `response_format=b64_json` to get the raw base64 in a `b64_json` field instead. ### Operations The `operation` field selects what to do. Support is per model: each model lists the operations it serves in `image_operations` on `GET /v1/models`. | Operation | Description | Requires | | - | - | - | | `edit` (default) | Edit guided by `prompt` (+ optional `mask` and reference images) | `prompt` | | `remove_background` | Strip the background; returns a transparent image | `image` only | | `upscale` | Increase resolution | Optional `upscale_factor` (`x2` / `x4`) or `quality_tier` (`standard` = x2, `hd` = x4) | | `outpaint` | Extend the canvas | `prompt`, plus a `mask` (Imagen) or `expand_factor` 1.01–3.0 (Gemini, maskless) | | `inpaint` | Repaint the masked region (white = edit) | `mask` with the same dimensions as `image`; optional `mask_feather` | | `mix` | Compose 2–4 images into one | `prompt` and 1–3 `reference_images`; returns one image | | `reframe` | Recompose to a new aspect ratio | `aspect_ratio`: `1:1`, `3:4`, `4:3`, `9:16`, `16:9`, `2:3` or `3:2`; returns one image | Requesting an operation the model doesn't support returns `422`. A provider with no image-edit support at all returns `501`. ### Request fields `multipart/form-data`: | Field | Type | Description | | - | - | - | | `model` | string | Image model ID (required) | | `image` | file | Source image: PNG, JPEG, or WebP (required). Size caps below | | `prompt` | string | Edit instruction. Required for `edit`, `outpaint` and `mix` | | `operation` | string | One of the [operations](#operations) above (default `edit`) | | `mask` | file | Region to edit (white = edit; Mesh converts to each provider's convention). Same formats as `image` | | `reference_images` | file\[] | Extra reference images (multi-image edit, `mix` sources). Models with `supports_image_reference: true` only | | `n` | integer | Number of images to return (default: 1). `mix` and `reframe` always return one | | `size` | string | Output dimensions — e.g. `"1024x1024"` or `"auto"` (default) | | `response_format` | string | `"url"` (default) or `"b64_json"` | | `background` | string | `"transparent"`, `"opaque"`, or `"auto"` | | `upscale_factor` | string | `upscale` only: `x2` or `x4` (BytePlus maps x2 → 2K, x4 → 4K) | | `quality_tier` | string | `upscale` only: `standard` (x2) or `hd` (x4). `upscale_factor` wins when both are set | | `aspect_ratio` | string | Target ratio. Required for `reframe`; also used by Gemini `edit` and Grok Imagine `edit`, `mix` and `reframe` | | `resolution` | string | xAI Grok Imagine only | | `expand_factor` | number | `outpaint` on Gemini: how much to grow the canvas, 1.01–3.0 | | `mask_feather` | integer | `inpaint` only: feather radius in pixels for blending the result back through the mask (default 0, off) | | `seed`, `guidance_scale`, `watermark`, `optimize_prompt` | — | BytePlus Seedream only, same as on generation | | `tags`, `safety_identifier` | — | Same as on generation | **Size caps.** Each image is capped at 20 MB, lower for some providers: BytePlus 4.7 MB, Gemini and Imagen 7 MB, Grok Imagine via OpenRouter 10 MB. Masks are capped at 4 MB on OpenAI, Azure and Imagen. All reference images together are capped at 20 MB. An oversized file returns `422`. The per-file caps sit under a hard **32 MiB** ceiling on the whole request body, enforced at the edge. A JSON body carries every image as base64, about 4/3 of the file size, so it reaches that ceiling sooner than multipart. An oversized request returns a plain HTML `413` page with no Mesh error envelope and no request ID. See [Rate Limits & Spend Caps](/docs/getting-started/rate-limits#request-body-size). ### More examples Remove a background: ```bash theme={null} curl https://api.meshapi.ai/v1/images/edits \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -F "model=openai/gpt-image-1" \ -F "operation=remove_background" \ -F "image=@product.png" ``` Edit with multiple reference images (BytePlus Seedream): ```bash theme={null} curl https://api.meshapi.ai/v1/images/edits \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -F "model=byteplus/seedream-5-0" \ -F "operation=edit" \ -F "prompt=Blend these into one product scene" \ -F "image=@base.png" \ -F "reference_images=@ref1.png" \ -F "reference_images=@ref2.png" ``` ## Streaming Set `"stream": true` to get a `text/event-stream` instead of a single JSON response: ```json theme={null} { "model": "openai/gpt-image-1-mini", "prompt": "...", "stream": true } ``` BytePlus Seedream 4.0, 4.5 and 5.0 (e.g. `byteplus/seedream-5-0`) stream natively, in BytePlus's own event shape. Each finished image arrives as an `image_generation.partial_succeeded` event with `url` or `b64_json` at the top level of the event, not under `data[]`. An image that fails arrives as `image_generation.partial_failed`. The stream ends with `image_generation.completed`, which carries `usage`; there is no `data: [DONE]`. For every other model the stream keeps the connection open during a long generation: the server emits an initial `processing` chunk, then SSE comment pings every 15 seconds until the image is ready, then one chunk with the result in `data[]`, then `data: [DONE]`. ### Keep-alive chunk sequence ``` data: {"id":"img_...","object":"image.chunk","created":...,"model":"...","data":[],"status":"processing"} : ping : ping data: {"id":"...","object":"image.chunk","created":...,"data":[{"url":"..."}]} data: [DONE] ``` ## Available models | Model | Notes | | - | - | | `openai/gpt-image-1-mini` | Fast, cost-efficient — ideal for most use cases | | `openai/gpt-image-1` / `openai/gpt-image-2` | GPT image models, strong instruction-following | | `google/gemini-3-pro-image` | Gemini 3 image: reference images, every edit operation | | `google/imagen-4-fast` | Google Imagen 4 Fast. Text-to-image, plus edit, inpaint, outpaint, upscale and remove\_background | | `byteplus/seedream-5-0` | BytePlus Seedream: reference images, native streaming | | `x-ai/grok-imagine` | xAI Grok Imagine: `aspect_ratio` / `resolution`, edit, mix, reframe | Check `GET /v1/models` for the full live list of enabled image models, their pricing, and each model's `image_operations`. # Images & Vision Source: https://developers.meshapi.ai/docs/capabilities/images-vision Send images to multimodal models, and generate images through chat completions or the images endpoints. Mesh API uses `POST /v1/chat/completions` for image input, and accepts image generation there too. For reference images, edit operations and streaming, use `POST /v1/images/generations` and `POST /v1/images/edits`; see [Image Generation](/docs/capabilities/image-generation). Use this guide for two common workflows: * Send image input to a multimodal model * Generate images from a text prompt ## Find image-capable models Call `GET /v1/models` and inspect: * `input_modalities` for image input support * `output_modalities` for image output support ## Send image input You can send images to multimodal models (like GPT-4o or Claude Sonnet 4.5) by passing an array of content parts in the message. Mesh API supports two ways to provide images: 1. **Base64 encoded data**: Use `data:image/jpeg;base64,...` URLs. 2. **Public HTTP URLs**: Use standard `https://...` URLs. Not all providers accept images by public URL. Requests routed to Google Vertex AI silently drop `http(s)` image URLs: the request still runs and is billed, but the model never sees the image. AWS Bedrock rejects `http(s)` image URLs with `422`. Use base64 `data:` URLs for maximum compatibility. Here is an example request sending an image via URL: ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "What is in this image?" }, { "type": "image_url", "image_url": { "url": "https://example.com/image.jpg", "detail": "auto" } } ] } ] }' ``` To send a base64 encoded image, replace the `url` value with the data URL: `"url": "data:image/jpeg;base64,iVBORw0KGgo..."` ## Generate images Use the same chat completions endpoint with image-generation-capable models. ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-image-1", "image": { "size": "1024x1024", "quality": "high", "response_format": "url" }, "messages": [ { "role": "user", "content": "Generate a watercolor of a fox in a snowy forest." } ] }' ``` The exact assistant payload may vary by model. Build against the documented response fields for the model you choose and the media format you request. ## Request tips * Use `detail: "low"` for lower-cost image understanding when supported. * Use hosted HTTPS URLs or `data:` URLs for image input. ## SDK coverage * Node: `client.chat.completions.create(...)` * Python: `client.chat.completions.create(...)` * Go: `client.Chat.Completions.Create(...)` * Java: `client.chat().completions().create(...)` # Memory Source: https://developers.meshapi.ai/docs/capabilities/memory Store per-user guardrails, preferences, and facts once, then attach them to any request with a single header. Memory lets you keep durable context about an end user — the tone they prefer, rules your assistant must never break, facts it should remember — outside your prompt. You store it once against a **memID**, then attach that memID to a chat completion with one header. Mesh composes the stored items into a leading system message before the request goes upstream. This is not conversation history. It is the small, stable set of things that should be true of *every* request for that user. ## Authentication Memory management accepts **either** authentication method: | Method | How | Typical use | | - | - | - | | **RSK key** (`rsk_...`) | `Authorization: Bearer rsk_...` | Your backend, storing facts as they come up | | **Mesh Dashboard** | Handled automatically | Reviewing and editing memory in the UI | Both resolve to the same owner, so the memories your backend writes are the ones the dashboard shows. Attaching a memory at inference time always uses your `rsk_...` key, the same as any other chat completion. *** ## Creating a memID ```bash theme={null} curl https://api.meshapi.ai/v1/memories \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "slug": "mem_user_8412", "name": "Preferences for user 8412", "description": "Tone, language, and standing facts." }' ``` The `slug` is the memID, the value you send in the `x-mem-id` header. Pick something stable and derived from your own user id: **it cannot be renamed**, because requests already sending it would stop resolving. A slug may not be UUID-shaped (that would be ambiguous with a memory id). Avoid spaces and commas: the slug travels in an HTTP header, and `x-mem-id` splits on commas. `description` is for your own reference and is never sent to the model. *** ## Adding items An item is a **guardrail**, a **preference**, or a **fact**. The three behave differently at request time, and choosing correctly matters more than anything else on this page. ```bash theme={null} curl https://api.meshapi.ai/v1/memories/mem_user_8412/items \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "item_type": "guardrail", "content": "Never discuss pricing for enterprise plans." }' ``` | Type | How it is used at request time | Key | | - | - | - | | **guardrail** | Always sent, in full, ahead of everything else. Never trimmed by the token budget, never expired by retention. | — | | **preference** | Sent in full, subject to the token budget. Deduplicated by `key` across attached memIDs. | Optional | | **fact** | Retrieved by relevance to the current request, within the token budget. Indexed for semantic search. | — | Guardrails are the only type with a completeness guarantee. Anything that must never be silently dropped — a compliance rule, a hard constraint — belongs in a `guardrail`, not a `preference`. A keyed preference is how you avoid contradictions when a request attaches more than one memID: ```bash theme={null} curl https://api.meshapi.ai/v1/memories/mem_user_8412/items \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "item_type": "preference", "key": "tone", "content": "Answer in two sentences, no preamble." }' ``` Facts are embedded when you add or edit them so they can be searched; that embedding is not billed. Guardrails and preferences are not embedded; they are always deterministic. *** ## Attaching memory to a request Send the memID in the `x-mem-id` header: ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -H "x-mem-id: mem_user_8412" \ -d '{ "model": "openai/gpt-4o-mini", "messages": [{ "role": "user", "content": "Draft a reply to this complaint." }] }' ``` The response tells you which memIDs actually contributed: ``` X-Mesh-Memory-Applied: mem_user_8412 ``` That header lists only memIDs whose content **survived** the token budget, so it is the answer to "did my memory actually reach the model", not just "did I ask for it". A second header, `X-Mesh-Memory-Tokens`, carries Mesh's estimate of how many prompt tokens the injected block added. ### Multiple memIDs Attach up to **16**, comma-separated. Order is precedence, left-most wins: ```bash theme={null} -H "x-mem-id: mem_user_8412,mem_org_defaults" ``` * All distinct **guardrails** from every memID ship, deduplicated by text. * **Preferences** sharing a `key` collapse to one — the left-most memID's value. * **Facts** from all attached memIDs compete for relevance within one budget. This is how you layer a per-user memory over org-wide defaults without the two contradicting each other. `x-mem-id` takes **slugs**, not memory ids. The management API accepts either a slug or a UUID, but the header always matches on slug. ### How facts are selected Guardrails and preferences are deterministic — the same items resolve on every request. Facts are not: Mesh embeds your latest user message and retrieves the facts closest to it, so a memID holding hundreds of facts contributes only the handful that bear on the question in front of it. That retrieval issues one embedding call, billed to your account and recorded in your usage as `memory_search_embed`. It is small, but it is not free — and it is one call per request, not one per attached memID. ### Token budget The assembled block is capped at roughly **1,500 tokens**. Guardrails are always kept in full, even if they alone exceed the cap, and they count toward it. Preferences, then facts, are added only while the whole block stays inside the cap; a line that would overflow is dropped. A memID whose every line was dropped is *not* listed in `X-Mesh-Memory-Applied`. Injected text becomes part of the real prompt sent to the provider, so **it is tokenized and billed like any other prompt content**. A large memID raises the cost of every request that attaches it. *** ## Retention By default items are kept until you delete them. Set `retention_days` and Mesh stops sending — then deletes — preferences and facts that age past it: ```bash theme={null} curl -X PATCH https://api.meshapi.ai/v1/memories/mem_user_8412 \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "retention_days": 30 }' ``` Three things to know: * **Guardrails are exempt.** They never expire. A retention policy is for stored personal data, and silently dropping a safety rule is the opposite of what you asked for. * **Existing items are re-dated from when they were created**, not from when you set the policy. Setting 30 days on a memory holding a 40-day-old fact expires that fact immediately. * **Expiry takes effect on read.** An expired item stops being sent straight away; the row is deleted by a nightly sweep shortly after. Send `"retention_days": null` to go back to keeping items forever. To bring back items that have already lapsed (before the sweep deletes them), call `POST /v1/memories/{slug}/items/revive-expired`. It gives every lapsed preference and fact in that memID a fresh deadline. *** ## Reviewing and editing ### Inspect a memID ```bash theme={null} curl https://api.meshapi.ai/v1/memories/mem_user_8412 \ -H "Authorization: Bearer " ``` Returns the memory plus its items, with `expires_at` on each and `item_counts` by type. ### List memIDs ```bash theme={null} curl "https://api.meshapi.ai/v1/memories?q=user_84&limit=50" \ -H "Authorization: Bearer " ``` `q` matches on slug and name; `has_item_type` (`guardrail`, `preference` or `fact`) keeps only memIDs holding that type; page with `limit` (max 200) and `offset`. Organisation owners and admins see every memID in the organisation; members see their own. `GET /v1/memories/counts` returns item totals by type over the same scope. ### Edit an item Only the fields you include change: ```bash theme={null} curl -X PATCH https://api.meshapi.ai/v1/memories/mem_user_8412/items/ \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "content": "Answer in one short paragraph." }' ``` `item_type` cannot be changed — it decides both how the item is composed and whether it is indexed for search. Delete and re-add instead. Editing a fact re-indexes it. ### Turn a memID off without deleting it ```bash theme={null} curl -X PATCH https://api.meshapi.ai/v1/memories/mem_user_8412 \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "is_active": false }' ``` Requests sending a disabled memID succeed and simply carry no memory. ### Delete ```bash theme={null} # one item curl -X DELETE https://api.meshapi.ai/v1/memories/mem_user_8412/items/ \ -H "Authorization: Bearer " # the whole memID, and everything in it curl -X DELETE https://api.meshapi.ai/v1/memories/mem_user_8412 \ -H "Authorization: Bearer " ``` Both return `204 No Content`. Deleting a memID removes its items and its stored facts from search, and is not reversible — it is the primitive to reach for when an end user asks you to erase what you hold about them. *** ## Usage ```bash theme={null} curl "https://api.meshapi.ai/v1/memories/usage?window_days=30" \ -H "Authorization: Bearer " ``` Per memID: requests, an estimate of the tokens the memory block added, when it was last used, and a per-key breakdown. Plus the count and cost of fact-search embeddings over the window. `injected_tokens_est` is Mesh's own count of the block it prepended — no provider reports the split between injected memory and the rest of your prompt. Treat it as an estimate, not a charge. `tracked_since` tells you how far back attribution data actually goes. *** ## Interactions worth knowing **Memory-attached requests are never cached.** A request whose `x-mem-id` memories were injected is personalized to one user, so serving it from the shared [response cache](/docs/capabilities/caching) could hand one user's context to another. Mesh bypasses the cache read *and* the cache write for these requests, unconditionally. If you lean on the gateway cache for cost control, attaching memory removes that saving entirely — you pay full price for every call. **Memory failures are silent by design.** If a memID does not exist, is inactive, belongs to someone else, or the lookup itself fails, the request proceeds *without* the memory rather than erroring. A memory problem can never take down your inference path — but a typo in `x-mem-id` produces a perfectly successful, completely un-personalized response. `X-Mesh-Memory-Applied` is how you tell those two apart. **Spend caps are re-checked after injection.** Injected tokens grow the prompt, so the cap is evaluated again against the assembled request: a key close to its limit can be rejected once memory is attached, even though the raw request would have passed. *** ## Who can see what | | You (the owner) | Org owner / admin | Other members | | - | - | - | - | | **See it in the list** | Yes | Yes | No | | **Read its items** | Yes | Yes | No | | **Delete it** | Yes | Yes | No | | **Edit / add items** | Yes | No | No | | **Attach with `x-mem-id`** | Yes | No | No | Two asymmetries are deliberate. Admins can **delete** a colleague's memID but not edit it, because deletion is the right-to-be-forgotten primitive and has to work after someone has left the company. And attachment stays with the owner: the gateway matches `x-mem-id` against the calling key's own owner, so an admin can read a memID in the dashboard that their own keys cannot use. The dashboard labels those rows rather than offering a header snippet that would quietly do nothing. *** ## Related * [Caching](/docs/capabilities/caching) — why memory-attached requests bypass the response cache * [Prompt Templates](/docs/capabilities/prompt-templates) — static, shared prompt scaffolding, versus memory's per-user context * [Usage & Monitoring API](/docs/reference/usage-api) — where `memory_search_embed` charges show up # Messages API (Anthropic-compatible) Source: https://developers.meshapi.ai/docs/capabilities/messages-api Point an Anthropic SDK at Mesh and reach every model in the catalog through /v1/messages. `POST /v1/messages` implements Anthropic's Messages API shape. If your code already speaks that format — through the Anthropic SDK or a hand-rolled client — you can switch the base URL to Mesh and keep the rest of the integration unchanged. The endpoint is **not restricted to Anthropic models**. Any model this gateway serves is reachable through it, so an existing Anthropic client can call an OpenAI, Google, or open-weight model without changing its request shape. For new code, [`/v1/chat/completions`](/docs/getting-started/quickstart) remains the recommended entry point — it has the widest feature coverage. ## Authentication Both header styles are accepted: ``` x-api-key: rsk_YOUR_KEY ``` ``` Authorization: Bearer rsk_YOUR_KEY ``` The `x-api-key` form is what Anthropic's own SDKs send, which is why an unmodified client works once the base URL is pointed at Mesh. ## Basic request ```bash theme={null} curl https://api.meshapi.ai/v1/messages \ -H "x-api-key: rsk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "anthropic/claude-sonnet-4.6", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Explain retrieval-augmented generation in two sentences."} ] }' ``` ```python theme={null} from anthropic import Anthropic client = Anthropic( api_key="rsk_YOUR_KEY", base_url="https://api.meshapi.ai", ) message = client.messages.create( model="anthropic/claude-sonnet-4.6", max_tokens=1024, messages=[ {"role": "user", "content": "Explain retrieval-augmented generation in two sentences."} ], ) print(message.content[0].text) ``` ```typescript theme={null} import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ apiKey: "rsk_YOUR_KEY", baseURL: "https://api.meshapi.ai", }); const message = await client.messages.create({ model: "anthropic/claude-sonnet-4.6", max_tokens: 1024, messages: [ { role: "user", content: "Explain retrieval-augmented generation in two sentences." }, ], }); console.log(message.content[0].text); ``` ## Streaming Set `stream: true` for Anthropic's SSE event sequence — `message_start`, `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop`. Anthropic's SDKs consume it unchanged. ```python theme={null} with client.messages.stream( model="anthropic/claude-sonnet-4.6", max_tokens=1024, messages=[{"role": "user", "content": "Count to five."}], ) as stream: for text in stream.text_stream: print(text, end="") ``` Token counts arrive on `message_delta`, not `message_start`. Anthropic reports input tokens on the first frame; Mesh only knows them once the upstream response completes, so `message_start.usage` carries zeros and `message_delta.usage` carries the real figures. SDKs that accumulate usage across the stream end up with the correct totals. ## Request fields | Field | Required | Notes | | - | - | - | | `model` | Yes | Any model in the Mesh catalog, in `provider/model-name` form | | `max_tokens` | Yes | Maximum tokens to generate — required by the Anthropic shape | | `messages` | Yes | `user` / `assistant` turns. A `system` turn is also accepted and keeps its position in the conversation | | `system` | No | System prompt, passed as a top-level field rather than a message | | `temperature` | No | Sampling temperature, `0`–`1` (Anthropic's range) | | `top_p` | No | Nucleus sampling, `0`–`1` | | `top_k` | No | Accepted for compatibility, but **not forwarded** — no provider mapping today | | `stop_sequences` | No | Strings that halt generation | | `stream` | No | `true` for SSE — see [Streaming](#streaming) | | `thinking` | No | Extended thinking: `{"type": "enabled", "budget_tokens": N}` with `N` between `1024` and `128000` (or omit `budget_tokens`). When enabled, the response carries `thinking` blocks, and a stream sends `thinking_delta` events | | `tools` / `tool_choice` | No | Anthropic-shaped tool definitions — see [Tool Calling](/docs/capabilities/tool-calling) | | `metadata` | No | `metadata.user_id` is recorded as the end-user identifier on the usage row. Not echoed back | | `safety_identifier` | No | Your end-user identifier, as on the other inference endpoints. Wins over `metadata.user_id` | | `tags` | No | Your own labels for the request, recorded on its usage row and never forwarded upstream | Content blocks may be `text`, `image`, `tool_use`, `tool_result` and `thinking`. `document` (PDF) blocks are not supported and return `400`. There is no `/v1/messages/count_tokens` endpoint. ## Errors Errors use Anthropic's envelope, so Anthropic SDKs raise their usual exception classes and back off on `429` as they would against Anthropic: ```json theme={null} { "type": "error", "error": { "type": "invalid_request_error", "message": "Request validation failed. ..." }, "request_id": "req_01ARZ3NDEKTSV4RRFFQ69G5FAV" } ``` A request that fails validation returns `400` `invalid_request_error`, as Anthropic's API does, not the `422` the OpenAI-shaped endpoints return. ## Which endpoint should I use? | Use case | Endpoint | | - | - | | New integration, widest feature coverage | `/v1/chat/completions` | | Existing Anthropic SDK code you don't want to rewrite | `/v1/messages` | | Claude Code | [`/v1/messages`](/docs/capabilities/claude-code) | | Reasoning effort, hosted tools, background jobs | [`/v1/responses`](/docs/capabilities/responses-api) | | Streaming output | either — both support SSE | ## Related * [Claude Code](/docs/capabilities/claude-code) — point the CLI at Mesh * [Tool Calling](/docs/capabilities/tool-calling) — the function-calling cycle * [Responses API](/docs/capabilities/responses-api) — reasoning effort and hosted tools * [Models & Pricing](https://meshapi.ai/models) — every model this endpoint can reach # Model Compare Source: https://developers.meshapi.ai/docs/capabilities/model-compare Compare multiple models with a single prompt. Use `POST /v1/chat/compare` when you want to run the same conversation across multiple models and inspect the results side by side. ## How it works 1. **Fan-out**: All requested models are called concurrently. The total wall-clock time is roughly that of the slowest model, not the sum of all models. 2. **Error isolation**: If a single model fails or times out (hard timeout of 120s), the others continue unaffected. Partial results are returned with a `partial: true` flag. 3. **Synthesis (default)**: After all models respond, a separate comparison LLM analyzes the responses and produces a structured evaluation covering accuracy, completeness, clarity, and a recommendation. Synthesis runs only when at least two models succeed. If every model fails, the request returns `502 all_models_failed` with the error nested under `detail` and no `request_id` (see [Compare troubleshooting](/debug/compare#the-whole-request-failed-with-502-all_models_failed)). When you don't name a `comparison_model`, the default judge is `openai/gpt-4o-mini`, falling back to other judges your key permits. If your key permits none, or every judge fails, `comparison` is `null`. 4. **Skip synthesis (optional)**: By setting `skip_comparison: true`, you can skip the synthesis step and receive only the raw model outputs. This is useful for parallel streaming UIs that perform their own comparison. 5. **Rate limiting and Billing**: The entire comparison counts as a single request against your rate limits (RPM/RPD). However, billing tracks each model call plus the comparison call as separate usage events (N+1 events). If the comparison model fails and a fallback judge takes over, each judge attempt is recorded as its own usage event, but only the judge that produced the synthesis is charged. 6. **Streaming**: Two streaming modes are available by setting `stream: true`. With synthesis enabled, fan-out is non-streaming, but the final comparison text is streamed token-by-token. If `skip_comparison: true` is set, each fan-out model streams its tokens in real-time concurrently, tagged by model name. ## Basic request ```bash theme={null} curl https://api.meshapi.ai/v1/chat/compare \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "models": [ "openai/gpt-4o-mini", "anthropic/claude-haiku-4.5" ], "messages": [ { "role": "user", "content": "Explain vector search in two sentences." } ] }' ``` ```ts theme={null} const result = await client.compare.create({ models: ["openai/gpt-4o-mini", "anthropic/claude-haiku-4.5"], messages: [{ role: "user", content: "Explain vector search in two sentences." }], }); console.log(result.results); ``` ```python theme={null} from meshapi import ChatMessage, CompareParams, MeshAPI client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") result = client.compare.create( CompareParams( models=["openai/gpt-4o-mini", "anthropic/claude-haiku-4.5"], messages=[ChatMessage(role="user", content="Explain vector search in two sentences.")], ) ) print(result.results) ``` ```go theme={null} result, err := client.Compare.Create(ctx, meshapi.CompareParams{ Models: []string{"openai/gpt-4o-mini", "anthropic/claude-haiku-4.5"}, Messages: []meshapi.ChatMessage{ {Role: "user", Content: "Explain vector search in two sentences."}, }, }) if err != nil { log.Fatal(err) } fmt.Println(result.Results) ``` ```java theme={null} var request = new com.meshapi.sdk.types.compare.CompareRequest(); request.models = java.util.List.of("openai/gpt-4o-mini", "anthropic/claude-haiku-4.5"); request.messages = java.util.List.of(ChatMessage.user("Explain vector search in two sentences.")); var result = client.compare().create(request); System.out.println(result.results); ``` ## Request fields | Field | Type | Notes | | - | - | - | | `models` | string\[] | Models to compare. Min 1, max 10. Duplicates are removed. | | `messages` | chat message\[] | Conversation sent to each model. | | `temperature` | number | Optional sampling temperature applied to every model. | | `max_tokens` | integer | Optional max completion tokens applied to every model. | | `model_overrides` | object\[] | Optional per-model overrides: a list of `{"model", "temperature", "max_tokens", "system_prompt"}` objects, one per model. | | `template`, `variables` | — | Accepted but not applied: compare does not render [prompt templates](/docs/capabilities/prompt-templates). Put the rendered text in `messages`. | | `cache` | boolean | Accepted and ignored: compare does not use the [response cache](/docs/capabilities/caching). | | `comparison_model` | string | Optional model used for synthesized comparison output. Default `openai/gpt-4o-mini`. | | `comparison_instructions` | string | Optional comparison rubric or guidance. | | `skip_comparison` | boolean | Return per-model outputs without synthesized comparison text. | | `stream` | boolean | Optional streaming mode. | | `tags` | object | Optional labels for the request, carried onto its usage events. | | `safety_identifier` | string | Optional end-user identifier for attribution. | ## Response shape ```json theme={null} { "comparison_id": "cmp_019ed1d24227774dba14", "object": "compare.completion", "created": 1781636687, "models": ["openai/gpt-4o-mini", "anthropic/claude-haiku-4.5"], "results": [ { "model": "openai/gpt-4o-mini", "response_body": { "choices": [...], "usage": {...} }, "content": "TCP is a connection-oriented protocol...", "latency_ms": 812, "error": null, "error_code": null, "usage": { "prompt_tokens": 24, "completion_tokens": 47, "total_tokens": 71 }, "request_id": "req_01ARZ3NDEKTSV4RRFFQ69G5FAV::openai/gpt-4o-mini" } ], "comparison": "Both answers are correct; Response 1 is more precise on ordering.", "comparison_model": "openai/gpt-4o-mini", "comparison_usage": { "prompt_tokens": 245, "completion_tokens": 405, "total_tokens": 650 }, "comparison_fallback_used": false, "total_latency_ms": 9016, "partial": false, "skip_comparison": false } ``` `results` is in `models` order; a failed model has `response_body`, `content` and `usage` null and `error` / `error_code` set. `comparison` is `null` when synthesis was skipped or fewer than two models succeeded. ## Streaming (SSE) Set `"stream": true` to receive a `text/event-stream` with typed events. There are two streaming modes: ### Mode 1: With comparison (`skip_comparison: false`, default) Fan-out models are non-streaming (full response collected per model), then the comparison LLM streams token-by-token. | Event | When | Payload | | - | - | - | | `meta` | Immediately after auth | `{"comparison_id", "models", "comparison_model", "skip_comparison": false}` | | `model_chunk` | As each fan-out model finishes | `{"model", "delta", "latency_ms", "error", "error_code", "usage"}` | | `model_done` | All fan-out results collected | `{"results": [...]}` | | `comparison_chunk` | During comparison LLM streaming | `{"delta": "", "finish_reason": null \| "stop"}` | | `done` | All complete | `{"comparison_id", "total_latency_ms", "partial", "comparison_model", "comparison_fallback_used"}` | ### Mode 2: Skip comparison (`skip_comparison: true`) Each fan-out model streams tokens in real time concurrently, tagged by model name. No comparison LLM is called. | Event | When | Payload | | - | - | - | | `meta` | Immediately after auth | `{"comparison_id", "models", "comparison_model": null, "skip_comparison": true}` | | `model_chunk` | Each token from any model | `{"model": "...", "delta": "Hello", "finish_reason": null}` | | `model_stream_done` | One model's stream ends | `{"model": "...", "finish_reason": null, "usage": {...}, "error": null \| "..."}`. Read the finish reason from that model's last `model_chunk` | | `done` | All models finished | `{"comparison_id", "total_latency_ms", "partial", "skip_comparison": true}` | ## SDK coverage * Node: `client.compare.create(...)` * Python: `client.compare.create(...)` * Go: `client.Compare.Create(...)` * Java: `client.compare().create(...)` Streaming compare is also available in the SDKs through their compare stream methods when you want incremental events instead of a single final JSON response. ## When to use compare * **Model selection** — evaluate candidate models and find the best cost/quality trade-off for your task * **Regression testing** — catch output quality regressions when a provider updates a model * **Prompt engineering** — see how different phrasings affect output across several models at once * **Provider comparison** — evaluate the same underlying model served by different providers * **Internal evaluations** — build prompt evals on a stable request shape All models in a compare request run in parallel — total latency is the slowest model's response time, not the sum of all of them. # Notifications Source: https://developers.meshapi.ai/docs/capabilities/notifications Subscribe to account events and receive them as signed HTTP callbacks or as email, in real time. Notifications let your org subscribe to account events — an API key revoked, a member added, your balance running low — and receive them the moment they happen, instead of polling the API or the dashboard. Each event is sent to one or more **destinations**. A destination is either a **webhook endpoint** — a URL of yours, which receives a signed HTTP `POST` — or an **email recipient**, an address that receives a readable message. Both are configured through the same API and share one delivery log. Webhook deliveries are signed with an HMAC so you can verify they came from Mesh, retried on failure with exponential backoff, and logged so you can inspect or manually redeliver any attempt. Alert configuration and the delivery log are management endpoints: they take an [admin key](/docs/getting-started/admin-keys) (`mak_...`) granted `alerts:read` / `alerts:write`, not your `rsk_` inference key. `$MESH_ADMIN_KEY` below is that key. ## Setting up a webhook endpoint ```bash theme={null} curl https://api.meshapi.ai/alerts/channels \ -H "Authorization: Bearer $MESH_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_type": "webhook", "destination": "https://example.com/webhooks/mesh", "is_enabled": true }' ``` **Response:** ```json theme={null} { "id": "9c1b1e3a-...", "channel_type": "webhook", "destination": "https://example.com/webhooks/mesh", "is_enabled": true, "signing_secret": "whsec_9f2a...b7c1" } ``` `signing_secret` is returned **exactly once**, on creation (and again on rotation — see [Rotating your signing secret](#rotating-your-signing-secret)). No `GET` ever returns it. Store it in your own secret manager immediately; if you lose it, rotate. `destination` must be a publicly resolvable `http(s)` URL — not `localhost`, and not an address that resolves to a private, loopback, or link-local range. This is checked when you register or update the endpoint, and again before every delivery attempt, so an endpoint that later starts resolving to an internal address stops receiving deliveries rather than silently forwarding them there. Each subscription is one `(event type → your endpoint)` pair. Create one policy per event type you want: ```bash theme={null} curl https://api.meshapi.ai/alerts/policies \ -H "Authorization: Bearer $MESH_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "9c1b1e3a-...", "alert_type": "api_key.revoked", "is_enabled": true }' ``` Some events are threshold-based. `balance.low` takes a USD floor: ```json theme={null} { "channel_id": "9c1b1e3a-...", "alert_type": "balance.low", "threshold_usd": 10.00 } ``` `spend_cap.approaching` and `rate_limit.threshold` take a percentage instead: ```json theme={null} { "channel_id": "9c1b1e3a-...", "alert_type": "spend_cap.approaching", "threshold_pct": 80 } ``` Every other event type takes no threshold — pass neither field. See [Available events](#available-events) for which is which; the API rejects a threshold supplied for a no-threshold event, and rejects a missing one for a threshold event. Once subscribed, matching events start arriving at your endpoint as signed `POST` requests. See [Verifying webhook signatures](#verifying-webhook-signatures) below before you trust any payload. Reading channels and policies needs `alerts:read`, and creating, updating, or deleting them needs `alerts:write`. Org owners and admins hold both, as can a custom role that grants them; plain members and viewers hold neither. Managing channels and policies: | Endpoint | Behavior | | - | - | | `GET /alerts` | List this org's channels and policies | | `POST /alerts/channels` | Register an endpoint. Returns `signing_secret` once | | `PATCH /alerts/channels/{id}` | Update the URL or enable/disable | | `DELETE /alerts/channels/{id}` | Remove the endpoint — also deletes its subscriptions and delivery history | | `POST /alerts/channels/{id}/rotate-secret` | Mint a new signing secret | | `POST /alerts/policies` | Subscribe the endpoint to an event type | | `PATCH /alerts/policies/{id}` | Change the threshold, target endpoint, or enabled state | | `DELETE /alerts/policies/{id}` | Unsubscribe | ## Sending events to an email address A destination does not have to be a service. An **email recipient** receives the same events as a readable message — no receiver to build, no signature to verify. ```bash theme={null} curl https://api.meshapi.ai/alerts/channels \ -H "Authorization: Bearer $MESH_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_type": "email", "destination": "ops@yourcompany.com" }' ``` Org owners and admins only, same as a webhook endpoint. ```bash theme={null} curl -X POST https://api.meshapi.ai/alerts/channels/CHANNEL_ID/verify \ -H "Authorization: Bearer $MESH_ADMIN_KEY" ``` We mail a confirmation link to that address. The link is **single-use and expires in 24 hours**; re-requesting mints a new one and voids the previous. The token is never returned to you — only the recipient can confirm. That is the point: it means nobody can sign a colleague up for mail they did not agree to. They follow the link. Until they do, `verified_at` on the channel stays `null` and **no events from this catalogue are delivered to that address.** Changing a channel's `destination` clears the confirmation — consent belongs to the address, not to the record — so a re-pointed channel must confirm again. Read the state back from `GET /alerts`, which lists every channel: ```json theme={null} { "id": "9c1b1e3a-...", "channel_type": "email", "destination": "ops@yourcompany.com", "is_enabled": true, "verified_at": "2026-08-15T09:30:00Z" } ``` **Email recipients cannot take threshold events yet** — `balance.low`, `spend_cap.approaching` and `rate_limit.threshold` each need a value you choose, and that is configured per webhook endpoint. Every other event in the catalogue can go to an address. Email deliveries appear in the same delivery log as webhooks, described below, and are retried the same way. ## Available events | Event | Fires when | Threshold | | - | - | - | | `api_key.created` | A new API key is created in your org | — | | `api_key.revoked` | An API key is revoked | — | | `member.added` | A member joins your org | — | | `member.removed` | A member is removed from (or leaves) your org | — | | `balance.low` | Your balance crosses below a USD floor you configure | `threshold_usd` (required, > 0) | | `balance.credited` | A payment or manual credit lands on your balance | — | | `auto_recharge.triggered` | An AutoPay recharge is **scheduled** — no money has moved yet | — | | `auto_recharge.succeeded` | An AutoPay charge clears and the credit lands on your balance | — | | `auto_recharge.failed` | An AutoPay recharge attempt fails | — | | `balance.depleted` | Your balance crosses zero. Fires once per crossing, not per request | — | | `payment.succeeded` | A payment is captured and credited. A fully-covering coupon does **not** fire this | — | | `payment.failed` | A payment attempt fails. Your balance is not changed | — | | `spend_cap.approaching` | A key's spend crosses a percentage of its configured spend cap | `threshold_pct` (required, 1–100) | | `spend_cap.hit` | A request is rejected because a spend cap has been reached, at any scope | — | | `rate_limit.threshold` | A request rate crosses a percentage of an RPM/RPD limit, at any scope | `threshold_pct` (required, 1–100) | | `provider_key.added` | A BYOK provider key is registered | — | | `provider_key.removed` | A BYOK provider key is deleted | — | | `byok_key.failed` | A BYOK provider key is rejected upstream for a credential, billing, or quota reason. Repeats for the same key and reason are suppressed for a cooldown | — | | `provider.fallback` | A request could not be served by its requested provider and another provider served it | — | | `batch.completed` | A batch job finishes successfully | — | | `batch.failed` | A batch job fails or expires. A batch you **cancelled** does not fire this | — | | `video.completed` | A video generation task finishes and the output is ready | — | | `video.failed` | A video generation task fails | — | Every delivery body is a JSON envelope: ```json theme={null} { "id": "evt_01j...", "type": "api_key.revoked", "created_at": "2026-08-03T10:04:11Z", "org_id": "5e3f2b10-...", "data": { "key_id": "3a7c9e21-...", "label": "prod-key", "masked_key": "rsk_...9f2a" } } ``` `data` for each event: | Event | `data` fields | | - | - | | `api_key.created` | `key_id`, `label`, `masked_key` | | `api_key.revoked` | `key_id`, `label`, `masked_key` | | `member.added` | `user_id`, `role` | | `member.removed` | `user_id`, `role` | | `balance.low` | `balance_usd` | | `balance.credited` | `amount_usd`, `balance_after`, `payment_event_id`, `source` (`payment`, `auto_recharge`, `admin_grant`, `coupon`) | | `auto_recharge.triggered` | `amount_usd`, `scheduled_payment_id` | | `auto_recharge.succeeded` | `payment_event_id`, `payment_id`, `amount_usd`, `current_balance_usd`, `recovered` | | `auto_recharge.failed` | `reason`, `amount_usd` | | `balance.depleted` | `balance_usd`, `balance_before` | | `payment.succeeded` | `payment_event_id`, `payment_id`, `provider`, `currency`, `amount_usd` | | `payment.failed` | `payment_event_id`, `payment_id`, `provider`, `currency`, `amount_usd`, `reason`, `failed_at` | | `spend_cap.approaching` | `scope`, `scope_id`, `key_id`, `spent_usd`, `cap_usd` | | `spend_cap.hit` | `scope`, `scope_id`, `key_id`, `cap_usd`, `spent_usd` | | `rate_limit.threshold` | `scope`, `scope_id`, `key_id`, `limit_type`, `limit` | | `provider_key.added` | `pk_id`, `provider`, `label` | | `provider_key.removed` | `pk_id`, `provider`, `label` | | `byok_key.failed` | `pk_id`, `provider`, `label`, `category` (`credential`, `billing`, `quota`), `upstream_status`, `model`, `fallback_used` | | `provider.fallback` | `request_id`, `model_requested`, `model_served`, `from_provider`, `to_provider`, `reason`, `upstream_status` | | `batch.completed` | `batch_id`, `status`, `model`, `provider`, `output_file_id` | | `batch.failed` | `batch_id`, `status`, `model`, `provider`, `output_file_id` (`null`) | | `video.completed` | `task_id`, `status`, `model`, `provider`, `video_url`, `error_code` (`null`), `error_message` (`null`) | | `video.failed` | `task_id`, `status`, `model`, `provider`, `video_url` (`null`), `error_code`, `error_message` | ### Which limit fired: `scope` Spend caps and rate limits are enforced at five scopes, not just per key. `spend_cap.*` and `rate_limit.threshold` therefore carry a `scope` discriminator and a `scope_id` naming the thing the limit is attached to: | `scope` | `scope_id` is | Also carries | | - | - | - | | `key` | the API key id | `key_id` (same value) | | `member` | the user id | — | | `team_member` | the user id | `team_id` | | `team` | the team id | — | | `org` | the organization id | — | `key_id` is always present and is `null` for every scope except `key`, so a receiver that filters on it keeps working and gets an explicit "this is not about one key" rather than a missing field. ```json theme={null} { "id": "evt_01j...", "type": "spend_cap.hit", "created_at": "2026-08-10T09:12:00Z", "org_id": "5e3f2b10-...", "data": { "scope": "org", "scope_id": "5e3f2b10-...", "key_id": null, "cap_usd": "500.0000", "spent_usd": "500.1200" } } ``` One request can trip more than one scope — a key cap and an org cap, say. Each scope is debounced independently, so you receive one event per scope rather than a single ambiguous one. `spend_cap.approaching` is emitted for the `key` scope only. It is computed from the per-key running total, which is the one counter with a before/after delta available at the moment the total changes; the hierarchy counters have no equivalent. `spend_cap.hit` covers all five scopes. A threshold event's `data` carries the values it crossed with (e.g. `spent_usd`/`cap_usd`), not the specific `threshold_pct`/`threshold_usd` you subscribed with — the same event is fanned out to every matching subscription on your endpoint, and each subscription can have its own threshold, so the envelope can't name "the" one that fired. If you have multiple subscriptions to the same event type at different thresholds, use `data` to compute which of yours applied. Each subscribed threshold is debounced on its own, so a ladder (say 50%, 80% and 95% on one key) delivers every rung it crosses rather than only the first. `data` only ever contains fields your org already has access to — never a plaintext API key or a provider credential. ## Verifying webhook signatures Every delivery carries these headers: | Header | Value | | - | - | | `Mesh-Event-Id` | The logical event's id — use this as your idempotency key | | `Mesh-Delivery-Id` | This delivery attempt's id | | `Mesh-Event-Type` | e.g. `api_key.revoked` | | `Mesh-Webhook-Timestamp` | Unix seconds when this attempt was sent | | `Mesh-Webhook-Signature` | `v1=` — see below | The signature is an HMAC-SHA256 of `{timestamp}.{raw_request_body}`, hex-encoded, using your endpoint's signing secret. This is the same construction Stripe uses, so if you already have a verifier for another vendor, the shape will look familiar. To verify a request: 1. Read the raw request body — **do not** re-serialize a parsed object. Any difference in key order, whitespace, or number formatting changes the bytes and the signature will not match. 2. Recompute the HMAC over `f"{timestamp}.{raw_body}"` using your signing secret. 3. Compare it against the `v1=` term(s) in `Mesh-Webhook-Signature` using a **constant-time** comparison — never `==`. A variable-time comparison leaks the correct signature one byte at a time through timing. 4. Reject the request if `Mesh-Webhook-Timestamp` is more than 5 minutes from your current time. The timestamp is inside the signed string, so an attacker can't replay an old, captured request with a new timestamp — but without this check, an old captured request replayed with its *original* timestamp would still verify. During a secret rotation grace period, `Mesh-Webhook-Signature` carries **two** comma-separated `v1=` terms — one for the new secret, one for the old. Accept the request if **either** term verifies, or your endpoint will reject deliveries for the duration of the rotation window. ```python theme={null} import hashlib import hmac import time TOLERANCE_SECONDS = 300 # 5 minutes def verify_signature(secret: str, timestamp: str, raw_body: bytes, signature_header: str) -> bool: if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS: return False signed_payload = f"{timestamp}.".encode() + raw_body expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest() # One v1= term per active secret — two during a rotation grace # window. Accept if ANY term matches. candidates = [ term.split("=", 1)[1] for term in signature_header.split(",") if term.startswith("v1=") ] return any(hmac.compare_digest(expected, candidate) for candidate in candidates) # Example: FastAPI handler @app.post("/webhooks/mesh") async def handle_mesh_webhook(request: Request): raw_body = await request.body() # raw bytes, not request.json() timestamp = request.headers["Mesh-Webhook-Timestamp"] signature = request.headers["Mesh-Webhook-Signature"] if not verify_signature(WEBHOOK_SECRET, timestamp, raw_body, signature): raise HTTPException(status_code=400, detail="Invalid signature") event = json.loads(raw_body) # handle event["type"], dedupe on event["id"] return {"received": True} ``` ```typescript theme={null} import crypto from "crypto"; const TOLERANCE_SECONDS = 300; // 5 minutes function verifySignature( secret: string, timestamp: string, rawBody: Buffer, signatureHeader: string, ): boolean { if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) { return false; } const signedPayload = Buffer.concat([Buffer.from(`${timestamp}.`), rawBody]); const expected = crypto.createHmac("sha256", secret).update(signedPayload).digest("hex"); const expectedBuf = Buffer.from(expected, "hex"); // One v1= term per active secret — two during a rotation grace // window. Accept if ANY term matches. const candidates = signatureHeader .split(",") .filter((term) => term.startsWith("v1=")) .map((term) => term.slice(3)); return candidates.some((candidate) => { const candidateBuf = Buffer.from(candidate, "hex"); return ( candidateBuf.length === expectedBuf.length && crypto.timingSafeEqual(expectedBuf, candidateBuf) ); }); } // Example: Express handler — mount with express.raw() so req.body is a Buffer, // not a parsed object. app.post( "/webhooks/mesh", express.raw({ type: "application/json" }), (req, res) => { const timestamp = req.header("Mesh-Webhook-Timestamp")!; const signature = req.header("Mesh-Webhook-Signature")!; if (!verifySignature(WEBHOOK_SECRET, timestamp, req.body, signature)) { return res.status(400).json({ error: "Invalid signature" }); } const event = JSON.parse(req.body.toString("utf8")); // handle event.type, dedupe on event.id res.json({ received: true }); }, ); ``` ```go theme={null} package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "math" "net/http" "strconv" "strings" "time" ) const toleranceSeconds = 300 // 5 minutes func verifySignature(secret, timestamp string, rawBody []byte, signatureHeader string) bool { ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > toleranceSeconds { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp + ".")) mac.Write(rawBody) expected := hex.EncodeToString(mac.Sum(nil)) // One v1= term per active secret — two during a rotation grace // window. Accept if ANY term matches. for _, term := range strings.Split(signatureHeader, ",") { term = strings.TrimSpace(term) if !strings.HasPrefix(term, "v1=") { continue } candidate := strings.TrimPrefix(term, "v1=") if hmac.Equal([]byte(expected), []byte(candidate)) { return true } } return false } func handleMeshWebhook(w http.ResponseWriter, r *http.Request) { rawBody, _ := io.ReadAll(r.Body) // raw bytes, not json.Decode straight from r.Body timestamp := r.Header.Get("Mesh-Webhook-Timestamp") signature := r.Header.Get("Mesh-Webhook-Signature") if !verifySignature(webhookSecret, timestamp, rawBody, signature) { http.Error(w, "invalid signature", http.StatusBadRequest) return } // unmarshal rawBody, handle event.Type, dedupe on event.ID w.WriteHeader(http.StatusOK) } ``` ## Delivery semantics * **At-least-once, never exactly-once.** The same event can arrive more than once — a retried attempt after a slow-but-successful response, or a manual redelivery. `Mesh-Event-Id` is your idempotency key: dedupe on it before acting on an event a second time. * **No ordering guarantee.** Deliveries for different events can arrive out of order. Order on the payload's `created_at`, not on arrival time. * **Success is 2xx only** (webhook endpoints). Any other response — including a redirect — counts as a failed attempt. Redirects are not followed. * **Retries with backoff.** The first attempt fires immediately. On failure, up to 5 more attempts follow — roughly `30s, 2m, 10m, 1h, 6h` after the previous one (jittered ±20%) — for 6 attempts total. After the last one fails, the delivery is marked `dead`: it stays visible in your delivery log and can be redelivered manually, but is not retried automatically again. * **Timeout.** Each attempt waits up to 10 seconds for your endpoint to respond. * **For email recipients**, at-least-once, ordering and the retry schedule are the same. A `succeeded` email delivery means the message was **accepted for sending** — it is not a receipt. A message accepted and then bounced by the receiving server is not currently reflected in the log, so treat the log as "we sent it", not "they got it". ## Inspecting deliveries | Endpoint | Behavior | | - | - | | `GET /v1/webhooks/deliveries` | List this org's deliveries. Filter by `channel_id`, `event_type`, `status`, `since`, `until`; cursor-paginated via `cursor` and `limit` | | `GET /v1/webhooks/deliveries/{id}` | Full detail for one delivery, including the exact payload that was (or will be) sent | | `POST /v1/webhooks/deliveries/{id}/redeliver` | Queue a fresh attempt for the same event. Org admin only. Returns `409` if the endpoint is disabled or deleted | | `GET /v1/webhooks/deliveries/stats` | Totals for a window (`window_hours`, default 24) plus a 7-day success rate per endpoint | | `POST /v1/webhooks/deliveries/test` | Send a sample payload for one `event_type` to one `channel_id`. Org admin only | ```bash theme={null} curl "https://api.meshapi.ai/v1/webhooks/deliveries?status=dead&event_type=api_key.revoked" \ -H "Authorization: Bearer $MESH_ADMIN_KEY" ``` **Response:** ```json theme={null} { "data": [ { "id": "b6e1a9c0-...", "event_id": "018f2e3a-...", "event_type": "api_key.revoked", "channel_id": "9c1b1e3a-...", "status": "dead", "attempt_count": 6, "response_status": 503, "last_error": "connect timeout", "next_attempt_at": null, "last_attempt_at": "2026-08-03T16:40:02Z", "created_at": "2026-08-03T10:04:11Z" } ], "next_cursor": null } ``` | Field | Meaning | | - | - | | `status` | `pending` (queued or awaiting retry), `delivering` (attempt in flight), `succeeded`, or `dead` (retries exhausted — redeliver manually if needed) | | `attempt_count` | Number of delivery attempts made so far | | `response_status` | HTTP status your endpoint returned on the last attempt, if any | | `last_error` | Error from the last attempt — a non-2xx body, or a transport error like a timeout | | `next_attempt_at` | When the next automatic retry is scheduled; `null` once the delivery is `succeeded` or `dead` | Redelivering creates a **new** delivery row with the same `event_id` and payload — the original attempt's history is never modified, so both remain in your log. ### Sending a test event ```bash theme={null} curl -X POST https://api.meshapi.ai/v1/webhooks/deliveries/test \ -H "Authorization: Bearer $MESH_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ "event_type": "api_key.created", "channel_id": "9c1b1e3a-..." }' ``` The test is a **real delivery**: it is signed, retried, dead-lettered and logged exactly like a live event, because a test that took a different code path would not tell you whether your real events will arrive. Your receiver can tell it apart by `test` on the payload: ```json theme={null} { "id": "evt_01j...", "type": "api_key.created", "test": true, "data": { "key_id": "3a7c9e21-...", "label": "Production", "masked_key": "rsk_...a1b2" } } ``` Org admins only. A paused endpoint answers `409`, and an email recipient answers `422` — test events go to webhook endpoints. ## Rotating your signing secret ```bash theme={null} curl -X POST https://api.meshapi.ai/alerts/channels/9c1b1e3a-.../rotate-secret \ -H "Authorization: Bearer $MESH_ADMIN_KEY" ``` **Response:** ```json theme={null} { "signing_secret": "whsec_ab12...ef90", "grace_until": "2026-08-04T10:00:00Z" } ``` Your old secret keeps verifying until `grace_until` (24 hours after rotation) — deliveries in that window are signed with **both** secrets (see the dual `v1=` terms above), so you can update your stored secret and redeploy without dropping any deliveries in between. After the grace window, only the new secret verifies. # Prompt Templates Source: https://developers.meshapi.ai/docs/capabilities/prompt-templates Store, version, and reuse prompt configurations — with variable substitution — across your team and apps. Prompt Templates let you define a system prompt, base conversation turns, and default inference parameters once — then reference them by name from any chat completion request. Instead of hardcoding the same prompt logic in every client, you manage it centrally and inject runtime values using `{{variable}}` slots. ## Authentication Template management (create, read, update, delete) accepts **either** authentication method: | Method | How | Typical use | | - | - | - | | **RSK key** (`rsk_...`) | `Authorization: Bearer rsk_...` | Direct API access from your backend | | **Mesh Dashboard** | Handled automatically | Managing templates through the dashboard UI | Using a template at inference time always uses your `rsk_...` key, the same as any other chat completion. See the [Authentication guide](/docs/getting-started/authentication) for details on key types. *** ## Creating a Template Send a `POST /v1/templates` request with a name, an optional system prompt, and any variables you want to parameterize. ```bash theme={null} curl https://api.meshapi.ai/v1/templates \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "support-agent", "description": "Customer support assistant with configurable tone", "system": "You are a {{tone}} support agent for {{company}}. Help users resolve their issues politely and accurately.", "model": "openai/gpt-4o-mini", "params": { "temperature": 0.5, "max_tokens": 512 }, "variables": ["company", "tone"] }' ``` ```ts theme={null} import { MeshAPI } from "meshapi-node-sdk"; const client = new MeshAPI({ baseUrl: "https://api.meshapi.ai", token: "rsk_...", }); const template = await client.templates.create({ name: "support-agent", description: "Customer support assistant with configurable tone", system: "You are a {{tone}} support agent for {{company}}. Help users resolve their issues politely and accurately.", model: "openai/gpt-4o-mini", params: { temperature: 0.5, max_tokens: 512 }, variables: ["company", "tone"], }); console.log(template.id); ``` ```python theme={null} from meshapi import CreateTemplateParams, MeshAPI client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") template = client.templates.create( CreateTemplateParams( name="support-agent", description="Customer support assistant with configurable tone", system="You are a {{tone}} support agent for {{company}}. Help users resolve their issues politely and accurately.", model="openai/gpt-4o-mini", params={"temperature": 0.5, "max_tokens": 512}, variables=["company", "tone"], ) ) print(template.id) ``` **Response:** ```json theme={null} { "id": "0197a3c2-5b1e-7d40-9a8f-2c6e1b4d7f30", "name": "support-agent", "owner": "user_abc123", "org_id": "0197a3c0-1f2a-7b3c-8d4e-5f6a7b8c9d0e", "team_id": "0197a3c0-2a3b-7c4d-9e5f-6a7b8c9d0e1f", "is_global": false, "description": "Customer support assistant with configurable tone", "system": "You are a {{tone}} support agent for {{company}}. Help users resolve their issues politely and accurately.", "messages": null, "model": "openai/gpt-4o-mini", "params": { "temperature": 0.5, "max_tokens": 512 }, "variables": ["company", "tone"], "category": null, "version": null, "is_beta": false, "creator_name": null, "creator_email": null, "created_at": "2026-06-01T10:00:00Z", "updated_at": "2026-06-01T10:00:00Z", "published_version": 1, "latest_version": 1 } ``` Template ids are UUIDs. Creating a template saves and publishes version 1. *** ## Using Variables Variables use double-brace syntax: `{{ variable_name }}`. Whitespace inside the braces is trimmed, so `{{company}}` and `{{ company }}` are equivalent. Variable names may contain letters, digits, and underscores. You can place variables in `system` and in any message `content` field: ```json theme={null} { "name": "onboarding-guide", "system": "You are onboarding {{user_name}} to {{product_name}}. Be concise and encouraging.", "messages": [ { "role": "assistant", "content": "Welcome, {{user_name}}! Let's get you set up with {{product_name}}." } ] } ``` The `variables` field (list of strings) is informational — it documents which slots the template expects. It doesn't enforce anything server-side, but it helps callers know what to pass. **Missing variables at inference time cause a `422` error.** Extra variables in the request are silently ignored. Variable values must be strings. *** ## Using a Template in Chat Completions Pass `template` (name or UUID) and `variables` alongside your `messages` in a `POST /v1/chat/completions` request. The template's system prompt and base messages are prepended to your messages before the request is forwarded to the model. ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "template": "support-agent", "variables": { "company": "Acme Corp", "tone": "friendly" }, "messages": [{ "role": "user", "content": "How do I reset my password?" }] }' ``` ```ts theme={null} const response = await client.chat.completions.create({ template: "support-agent", variables: { company: "Acme Corp", tone: "friendly" }, messages: [{ role: "user", content: "How do I reset my password?" }], }); console.log(response.choices[0]?.message.content); ``` ```python theme={null} from meshapi import ChatCompletionParams, ChatMessage response = client.chat.completions.create( ChatCompletionParams( template="support-agent", variables={"company": "Acme Corp", "tone": "friendly"}, messages=[ChatMessage(role="user", content="How do I reset my password?")], ) ) print(response.choices[0].message.content) ``` The final messages sent to the model look like this: ```json theme={null} [ { "role": "system", "content": "You are a friendly support agent for Acme Corp. Help users resolve their issues politely and accurately." }, { "role": "user", "content": "How do I reset my password?" } ] ``` You can reference a template by **name** or **UUID**. Name lookups are scoped to your key's owner first, then fall back to global (admin-seeded) templates. UUID lookups work across any owner. Any value that isn't a UUID is treated as a name. By default a request uses the template's **published** version. To freeze on a specific release, add `template_version` (a published version number) next to `template`; a version that doesn't exist or was never published returns `404`. *** ## Parameter Priority When a template and a request body both specify inference settings, Mesh resolves them in this order (highest wins): | Priority | Source | Example | | - | - | - | | 3 — highest | Request body | `"temperature": 0.9` in the POST body | | 2 | Key defaults | Default params configured on your API key | | 1 — lowest | Template defaults | `"params": { "temperature": 0.5 }` in the template | This means you can define sensible defaults in the template and still override them per-request without changing the template. The `model` resolves the same way: the request body's `model`, then your key's default model, then the template's. *** ## Managing Templates ### List all templates ```bash theme={null} curl https://api.meshapi.ai/v1/templates \ -H "Authorization: Bearer " ``` Returns your owned templates plus any global templates, ordered newest first. Add `?scope=custom` for the non-global templates you can see, or `?scope=global` for only the global templates your organization has enabled (the default is `all`). ### Get a single template ```bash theme={null} curl https://api.meshapi.ai/v1/templates/ \ -H "Authorization: Bearer " ``` ### Update a template `PATCH` changes metadata: `name` and `description`. Only the fields you include are changed. ```bash theme={null} curl -X PATCH https://api.meshapi.ai/v1/templates/ \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "description": "Support assistant, v2 tone guide" }' ``` The content fields (`system`, `messages`, `model`, `params`, `variables`) are still accepted on `PATCH`, but deprecated: sending them saves **and publishes** a new version in one step. Use the version endpoints below to change content. ### Versions Content is versioned. Every save creates a new, immutable version; only the **published** version serves inference. | Action | Endpoint | | - | - | | Save a new version (unset fields carry over from the latest) | `POST /v1/templates/{id}/versions` | | List versions | `GET /v1/templates/{id}/versions` | | Get one version | `GET /v1/templates/{id}/versions/{version_id}` | | Publish the latest version | `POST /v1/templates/{id}/publish` | | Roll back: copy an old version into a new one and publish it | `POST /v1/templates/{id}/versions/{version_id}/rollback` | ```bash theme={null} curl https://api.meshapi.ai/v1/templates//versions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "params": { "temperature": 0.3, "max_tokens": 1024 } }' curl -X POST https://api.meshapi.ai/v1/templates//publish \ -H "Authorization: Bearer " ``` Saving never changes what's live until you publish. A key with read-only template access sees only published versions. ### Delete a template ```bash theme={null} curl -X DELETE https://api.meshapi.ai/v1/templates/ \ -H "Authorization: Bearer " ``` Returns `204 No Content` on success. *** ## Global vs. Owned Templates | | Owned | Global | | - | - | - | | **Created by** | You (via `rsk_...`) | Mesh admins | | **Name lookup scope** | Your owner only | Available to all users | | **Can edit / delete** | Yes | No | | **UUID lookup** | Shareable by ID | Shareable by ID | Global templates are useful for standard system roles (e.g., a generic coding assistant or translation prompt) that Mesh pre-seeds for all users. If you create a template with the same name as a global one, your owned template takes precedence in name lookups. UUID-based lookups are intentionally cross-owner — this lets you share a template with another team by passing them the template ID. Name-based lookups are always scoped to your own account, so there's no risk of accidentally using someone else's template by name. # Files & RAG Source: https://developers.meshapi.ai/docs/capabilities/rag Upload your documents, search them semantically, and ground AI answers in your own content — all through the Mesh API. RAG lets you upload files, automatically convert them into searchable chunks, and then query them with plain-language questions. The API returns the most relevant passages, which you can feed directly into a chat completion to get answers grounded in your own content. The full flow is three steps: 1. **Upload** a file and get a `file_id` 2. **Wait** for embeddings to finish processing 3. **Search** with a natural-language query ```http theme={null} POST https://api.meshapi.ai/v1/files # upload POST https://api.meshapi.ai/v1/files/embed # (re-)trigger embeddings POST https://api.meshapi.ai/v1/files/search # semantic search ``` *** ## Step 1 — Upload a file Call `POST /v1/files` to register the file. You get back a `file_id` and a short-lived `signed_url` — use the signed URL to PUT the actual bytes directly to storage. ```bash theme={null} # 1a. Register the file and get an upload URL INIT=$(curl -s https://api.meshapi.ai/v1/files \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "file_name": "handbook.pdf", "mime_type": "application/pdf", "embed": true }') FILE_ID=$(echo "$INIT" | jq -r '.file_id') SIGNED_URL=$(echo "$INIT" | jq -r '.signed_url') # 1b. Upload the bytes, sending every header in .required_headers as-is HEADERS=() while IFS= read -r h; do HEADERS+=(-H "$h"); done < <( echo "$INIT" | jq -r '.required_headers | to_entries[] | "\(.key): \(.value)"' ) curl -X PUT "$SIGNED_URL" "${HEADERS[@]}" --data-binary @handbook.pdf echo "File ID: $FILE_ID" ``` ```python theme={null} import httpx client = httpx.Client(headers={"Authorization": "Bearer rsk_..."}) # Register the file init = client.post( "https://api.meshapi.ai/v1/files", json={ "file_name": "handbook.pdf", "mime_type": "application/pdf", "embed": True, }, ).json() file_id = init["file_id"] signed_url = init["signed_url"] # Upload bytes to the signed URL. No auth header; send required_headers verbatim. with open("handbook.pdf", "rb") as f: httpx.put(signed_url, content=f.read(), headers=init["required_headers"]) print(f"File ID: {file_id}") ``` ```ts theme={null} import fs from "fs"; const init = await fetch("https://api.meshapi.ai/v1/files", { method: "POST", headers: { Authorization: "Bearer ", "Content-Type": "application/json", }, body: JSON.stringify({ file_name: "handbook.pdf", mime_type: "application/pdf", embed: true, }), }).then((r) => r.json()); const { file_id, signed_url, required_headers } = init; // Upload bytes directly to the signed URL, sending required_headers verbatim await fetch(signed_url, { method: "PUT", headers: required_headers, body: fs.readFileSync("handbook.pdf"), }); console.log("File ID:", file_id); ``` ### Init upload fields | Field | Type | Notes | | - | - | - | | `file_name` | string | Name of the file (used for display and file-type detection) | | `mime_type` | string | MIME type, e.g. `application/pdf`, `text/plain`, `text/markdown` | | `embed` | boolean | Default `true` — automatically triggers embedding after upload | | `metadata` | object | Optional key-value pairs attached to every chunk (useful for filtering later) | ### Init upload response ```json theme={null} { "file_id": "0197a3c2-5b1e-7d40-9a8f-2c6e1b4d7f30", "signed_url": "https://storage.googleapis.com/...", "expires_at": "2026-05-26T12:15:00Z", "required_headers": { "Content-Type": "application/pdf" }, "max_size_bytes": 52428800 } ``` The `signed_url` expires 15 minutes after you get it. PUT the bytes right away, and send every header in `required_headers` exactly as given: they are part of the signature, and a missing or changed one makes storage reject the upload. Files are capped at `max_size_bytes` (50 MB). File ids are UUIDs. RAG needs a key created in the dashboard: a key whose owner isn't a user (for example one minted with an admin key) gets `400` on upload. *** ## Step 2 — Wait for embeddings After the PUT completes, the API automatically chunks and embeds your file (because `embed: true`). You can poll `GET /v1/files/{file_id}` to watch progress. Give the loop a deadline: if the PUT never lands, `upload_status` stays `pending` forever. ```bash theme={null} curl https://api.meshapi.ai/v1/files/$FILE_ID \ -H "Authorization: Bearer " ``` ```python theme={null} import time deadline = time.monotonic() + 600 while True: status = client.get(f"https://api.meshapi.ai/v1/files/{file_id}").json() print(status["upload_status"], status["embedding_status"]) if status["upload_status"] == "failed" or status["embedding_status"] in ("ready", "failed"): break if time.monotonic() > deadline: raise TimeoutError(f"file {file_id} not ready after 10 minutes") time.sleep(3) ``` ```ts theme={null} const poll = async () => { const deadline = Date.now() + 10 * 60 * 1000; while (true) { const status = await fetch( `https://api.meshapi.ai/v1/files/${file_id}`, { headers: { Authorization: "Bearer " } } ).then((r) => r.json()); console.log(status.upload_status, status.embedding_status); if (status.upload_status === "failed" || ["ready", "failed"].includes(status.embedding_status)) break; if (Date.now() > deadline) throw new Error(`file ${file_id} not ready after 10 minutes`); await new Promise((r) => setTimeout(r, 3000)); } }; await poll(); ``` ### Status fields | Field | Values | Meaning | | - | - | - | | `upload_status` | `pending` → `ready` \| `failed` | File bytes have been received and checked | | `embedding_status` | `queued` → `processing` → `ready` \| `failed` (starts at `pending` when `embed` is `false`) | Chunking and embedding progress | Once `embedding_status` is `ready`, the file is ready to search. `upload_status` goes to `failed` when the uploaded file is over 50 MB or its `Content-Type` doesn't match the `mime_type` you registered. Register the file again. The status object also carries `file_name`, `file_type`, `mime_type`, `size_bytes`, `created_at`, `updated_at`, `total_tokens`, `total_cost_usd` and `last_error_code`. *** ## Step 3 — Search your files Send a natural-language query to `POST /v1/files/search`. The API embeds your query, runs a hybrid search (dense vectors plus keyword matching, merged by reciprocal rank fusion), and returns the raw text with relevance scores. The query embedding is billed to your account like any embedding call. ```bash theme={null} curl https://api.meshapi.ai/v1/files/search \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "What is the refund policy?", "top_k": 5 }' ``` ```python theme={null} results = client.post( "https://api.meshapi.ai/v1/files/search", json={ "query": "What is the refund policy?", "top_k": 5, }, ).json() for r in results["results"]: print(f"[{r['score']:.3f}] {r['text'][:200]}") ``` ```ts theme={null} const results = await fetch("https://api.meshapi.ai/v1/files/search", { method: "POST", headers: { Authorization: "Bearer ", "Content-Type": "application/json", }, body: JSON.stringify({ query: "What is the refund policy?", top_k: 5, }), }).then((r) => r.json()); for (const r of results.results) { console.log(`[${r.score.toFixed(3)}] ${r.text.slice(0, 200)}`); } ``` ### Search fields | Field | Type | Notes | | - | - | - | | `query` | string | Plain-language question or phrase | | `top_k` | integer | Number of chunks to return (1–50, default 5) | | `file_ids` | string\[] | Restrict search to specific files; omit to search all your files | | `filter` | object | Match chunks by metadata key-value pairs (exact match) | | `date_from` | integer | Unix timestamp — only return chunks created after this time | | `date_to` | integer | Unix timestamp — only return chunks created before this time | ### Search response ```json theme={null} { "results": [ { "score": 0.912, "text": "Refunds are processed within 5–7 business days...", "parent_text": "Section 4: Returns and Refunds\n\nRefunds are processed...", "file_id": "0197a3c2-5b1e-7d40-9a8f-2c6e1b4d7f30", "file_name": "handbook.pdf", "chunk_index": 14, "metadata": {} } ] } ``` *** ## End-to-end: RAG chat Combine search results with a chat completion to answer questions from your documents. ```python theme={null} import httpx, json API = "https://api.meshapi.ai" HEADERS = {"Authorization": "Bearer rsk_...", "Content-Type": "application/json"} client = httpx.Client(headers=HEADERS) question = "What is the refund policy?" # 1. Retrieve relevant chunks search = client.post(f"{API}/v1/files/search", json={ "query": question, "top_k": 3, }).json() context = "\n\n".join(r["text"] for r in search["results"]) # 2. Ask the model chat = client.post(f"{API}/v1/chat/completions", json={ "model": "openai/gpt-4o-mini", "messages": [ { "role": "system", "content": f"Answer using only the context below.\n\n{context}", }, {"role": "user", "content": question}, ], }).json() print(chat["choices"][0]["message"]["content"]) ``` ```ts theme={null} const API = "https://api.meshapi.ai"; const headers = { Authorization: "Bearer ", "Content-Type": "application/json", }; const question = "What is the refund policy?"; // 1. Retrieve relevant chunks const search = await fetch(`${API}/v1/files/search`, { method: "POST", headers, body: JSON.stringify({ query: question, top_k: 3 }), }).then((r) => r.json()); const context = search.results.map((r: any) => r.text).join("\n\n"); // 2. Ask the model const chat = await fetch(`${API}/v1/chat/completions`, { method: "POST", headers, body: JSON.stringify({ model: "openai/gpt-4o-mini", messages: [ { role: "system", content: `Answer using only the context below.\n\n${context}`, }, { role: "user", content: question }, ], }), }).then((r) => r.json()); console.log(chat.choices[0].message.content); ``` *** ## Filtering by metadata Tag files at upload time and filter at search time — useful when you have documents from different departments, clients, or time periods. ```bash theme={null} # Upload with metadata curl https://api.meshapi.ai/v1/files \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "file_name": "q4-report.pdf", "mime_type": "application/pdf", "metadata": { "department": "finance", "quarter": "Q4-2025" } }' # Search only finance docs curl https://api.meshapi.ai/v1/files/search \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "What were the operating expenses?", "filter": { "department": "finance" } }' ``` *** ## Re-triggering embeddings If `embed` was set to `false` at upload time, or if embedding failed, you can kick it off manually: ```bash theme={null} curl https://api.meshapi.ai/v1/files/embed \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "file_ids": ["0197a3c2-5b1e-7d40-9a8f-2c6e1b4d7f30", "0197a3c2-6c2f-7e51-8b9a-3d7f2c5e8a41"] }' ``` The call returns `202` with a per-file status: ```json theme={null} { "results": [ { "file_id": "0197a3c2-5b1e-7d40-9a8f-2c6e1b4d7f30", "embedding_status": "queued", "chunk_count": null, "error": null }, { "file_id": "0197a3c2-6c2f-7e51-8b9a-3d7f2c5e8a41", "embedding_status": "queued", "chunk_count": null, "error": null } ] } ``` You can pass `"wait": true` to block until all embeddings finish (useful for small files in scripts). Rules, per file: * `file_ids` takes 1 to 100 ids. * The file's `upload_status` must be `ready`. * A file that is already embedded comes back as `completed`; one still queued or processing comes back as `processing`. * After a failed embedding, the same file can't be re-queued for 30 minutes. The entry comes back with `embedding_status: "error"` and an `error` saying when to retry. *** ## List your files ```bash theme={null} curl "https://api.meshapi.ai/v1/files?limit=20&offset=0" \ -H "Authorization: Bearer " ``` Response includes `files` (array of file status objects), `total`, `limit`, and `offset`. *** ## Delete a file Call `DELETE /v1/files/{file_id}` to remove a file. Its chunks are dropped from the search index and the stored file is deleted, so the document stops appearing in `POST /v1/files/search` results. ```bash theme={null} curl -X DELETE https://api.meshapi.ai/v1/files/$FILE_ID \ -H "Authorization: Bearer " ``` ```python theme={null} resp = client.delete(f"https://api.meshapi.ai/v1/files/{file_id}") resp.raise_for_status() print(resp.status_code) # 204 ``` ```ts theme={null} const resp = await fetch(`https://api.meshapi.ai/v1/files/${file_id}`, { method: "DELETE", headers: { Authorization: "Bearer " }, }); console.log(resp.status); // 204 ``` A successful delete returns **`204 No Content`** with an empty body — there is no JSON response to parse. | Response | Meaning | | - | - | | `204` | Deleted. Also returned for a file that was already deleted, so the call is safe to repeat | | `404` | No file with that `file_id` exists on your account | | `5xx` | The delete did not complete. Retry the call | Deleting is idempotent, so retrying is always safe and converges on a full delete. Treat a file as deleted only once you have received a `204` — if a call fails, repeat it until it succeeds rather than assuming it partly applied. Deletion is permanent. The file's chunks cannot be recovered, and searching it again means re-uploading and re-embedding the document from scratch. Usage records that reference the file stay available afterwards, so your billing history and [usage queries](/docs/reference/usage-api) remain complete. *** ## Common errors | HTTP | Meaning | | - | - | | `401` | Missing or invalid API key | | `402` | Balance or spend limit exceeded | | `404` | No file with that `file_id` on your account | | `422` | Invalid request body (check required fields) | | `429` | Rate limit exceeded — back off and retry | *** ## File management | Action | Endpoint | | - | - | | Upload (init signed URL) | `POST /v1/files` | | List | `GET /v1/files` | | Get file status | `GET /v1/files/{file_id}` | | Trigger embedding | `POST /v1/files/embed` | | Search | `POST /v1/files/search` | | Delete | `DELETE /v1/files/{file_id}` | ## Supported file types | Type | Extensions / MIMEs | | - | - | | PDF | `.pdf` | | Plain text | `.txt`, `.md`, and all `text/*` variants | | Word | `.docx`, `.doc` | | PowerPoint | `.pptx` | | Excel | `.xlsx`, `.xls` | | CSV / TSV | `.csv`, `.tsv` | | JSON | `.json` | | XML | `.xml` | | HTML | `.html` | | Code files | `.js`, `.ts`, `.py`, `.java`, `.c`, `.cpp`, `.cs`, `.go`, `.rb`, `.php`, `.sh`, `.sql` | | Other | `.parquet`, `.tex` | # Realtime Audio Source: https://developers.meshapi.ai/docs/capabilities/realtime-audio Bi-directional speech-to-speech and realtime speech-to-text over WebSocket. ## Overview `wss://api.meshapi.ai/v1/realtime` is a WebSocket gateway for low-latency realtime audio. It supports two modes: bidirectional speech-to-speech via OpenAI's Realtime API (served by OpenAI or Azure AI Foundry), and realtime speech-to-text via ElevenLabs Scribe. * Same auth surface as the rest of Mesh — your `rsk_...` data-plane token. * For speech-to-speech models the wire format is **identical** to OpenAI's Realtime API. Mesh passes JSON event bodies through verbatim in both directions, so any client written against the upstream spec works against Mesh by switching the WebSocket URL. `elevenlabs/scribe_v2_realtime` speaks ElevenLabs' Scribe frames instead. See [Speech to Text](/docs/capabilities/speech-to-text#elevenlabs-models). * Billed on usage, settled every 60 seconds during the session and at close. It's intended for voice agents, live transcription with response, and any half-duplex / full-duplex audio UX where round-trip latency matters. ## Quickstart Open a WebSocket to `wss://api.meshapi.ai/v1/realtime` with a `model` query parameter, send a `session.update`, then stream audio chunks via `input_audio_buffer.append` events. ```ts theme={null} // Browsers can't set request headers on a WebSocket, so pass the key // via the api_key query parameter. const ws = new WebSocket( `wss://api.meshapi.ai/v1/realtime?model=openai/gpt-realtime-2&api_key=${MESH_API_KEY}`, ); ws.addEventListener("open", () => { ws.send(JSON.stringify({ type: "session.update", session: { type: "realtime", output_modalities: ["audio"], audio: { output: { format: { type: "audio/pcm", rate: 24000 }, voice: "alloy" }, }, }, })); }); ws.addEventListener("message", (e) => { const event = JSON.parse(e.data); if (event.type === "response.output_audio.delta") { playAudio(event.delta); } }); ``` ```bash theme={null} # Use websocat (or wscat) for a quick connectivity check. The key goes in # the subprotocol or ?api_key=; an Authorization header is not read. websocat \ "wss://api.meshapi.ai/v1/realtime?model=openai/gpt-realtime-2&api_key=$MESH_API_KEY" ``` ## Authentication Two methods are supported. Both require TLS — `ws://` is rejected. **1. Subprotocol header (preferred).** Send your key in the `Sec-WebSocket-Protocol` header alongside the literal protocol marker: ``` Sec-WebSocket-Protocol: openai-realtime, Bearer ``` This matches OpenAI's upstream auth and keeps the key out of URL logs. **2. Query string fallback.** For browsers and tools that can't customize the subprotocol cleanly, append `?api_key=` to the URL. The key is redacted from server access logs but **will** appear in client-side history, so prefer the subprotocol method anywhere you control the runtime. ``` wss://api.meshapi.ai/v1/realtime?model=openai/gpt-realtime-2&api_key=rsk_... ``` If both are present, the subprotocol wins. ## Message protocol Mesh is a **transparent proxy** for OpenAI's Realtime API. Every event you send and receive is shaped exactly as upstream documents it — Mesh does not rewrite event types, field names, or payloads. See the [OpenAI Realtime API reference](https://platform.openai.com/docs/api-reference/realtime) for the full event catalog. The most common types you'll exchange: | Direction | Event | Purpose | | - | - | - | | Client → | `session.update` | Configure `output_modalities`, `audio` (input/output format, voice), instructions, tools | | Client → | `input_audio_buffer.append` | Append base64 PCM16 audio chunk | | Client → | `input_audio_buffer.commit` | Mark end of an utterance | | Client → | `response.create` | Ask the model to respond | | Server → | `response.output_audio.delta` | Streamed audio chunk back (base64) | | Server → | `response.output_text.delta` | Streamed text token | | Server → | `response.output_audio_transcript.delta` | Streamed transcript of output audio | | Server → | `response.done` | Response complete; usage included | | Server → | `error` | OpenAI-shaped error envelope | Realtime uses OpenAI's **GA** event protocol. `session.update` must use the GA shape (`{"type":"realtime","output_modalities":[...],"audio":{...}}`) — the older beta shape (`modalities`/`voice`/`input_audio_format`) is rejected. Audio travels as base64 inside JSON text events, as OpenAI's protocol defines. Mesh forwards binary WebSocket frames unchanged, but send audio as `input_audio_buffer.append` events. ## Supported models The `model` query parameter is required. Realtime supports two provider modes: | Model ID | Mode | | - | - | | `openai/gpt-realtime-2.1` | Speech-to-speech | | `openai/gpt-realtime-2.1-mini` | Speech-to-speech | | `openai/gpt-realtime-2` | Speech-to-speech | | `openai/gpt-realtime-1.5` | Speech-to-speech | | `openai/gpt-realtime` | Speech-to-speech | | `openai/gpt-realtime-mini` | Speech-to-speech | | `openai/gpt-realtime-translate` | Speech-to-speech translation | | `openai/gpt-realtime-whisper` | Realtime transcription | | `elevenlabs/scribe_v2_realtime` | Realtime speech-to-text | Consult `GET /v1/models` for the current set of realtime-capable models — the catalog is live and this table is a snapshot. ## Pricing **MeshAPI charges OpenAI's exact rates for realtime models — zero markup, no surprises.** Rates are subject to OpenAI's pricing changes; consult `/v1/models` for the canonical current values. Per-session usage and cost lands in your existing `/v1/usage` history and the dashboard. There is no in-stream cost message on the WebSocket — query `/v1/usage` after the session completes. ### Pre-flight pricing The `/v1/models` response indicates which models support realtime and includes per-million-token rates for text, audio, and cached audio inputs and outputs. Use these to display estimates in your UI before opening a session — no need to hard-code rates in the client. ## Billing * **Balance hold.** Opening a session holds up to \$10 of your balance until it closes (less for models priced by duration or sessions with a token cap; no hold on BYOK). If your balance can't cover the hold, the session is refused with `insufficient_quota`. If your account balance is exhausted during a session, the connection is closed with an `insufficient_quota` error. Top up to reconnect. Partial responses up to the point of disconnect are still billed. * **Concurrent sessions.** An account can hold 5 concurrent realtime sessions by default (a model can set its own cap in `realtime_max_concurrent_per_owner` on `GET /v1/models`). The count is shared with streaming TTS and STT sessions. * **Session token caps.** Sessions configured with a max-token cap close with a `session_token_cap_exceeded` error once the cap is reached. Usage is settled into your account's usage log every 60 seconds while the session is open and again at close, accessible via `GET /v1/usage` and the dashboard. Sessions that get cut short (network drop, browser tab close) still bill for the tokens that were processed. Query `/v1/usage` for canonical numbers. ## Limits and known caveats * **Session length.** Mesh closes a session after 30 minutes with `session_duration_exceeded`. Long-running agents should reconnect and resume application-level state. * **Idle timeout.** Mesh closes a socket that receives no client→server frame for 60 seconds, with `idle_timeout`. Send a `session.update` ping or keep the audio buffer flowing. * **Browsers / Safari.** Browsers can't set request headers on a WebSocket and can't reliably send the space-separated `Bearer ` subprotocol token. In the browser, authenticate with the `?api_key=` query fallback. * **No HTTP fallback.** This endpoint only exists as a WebSocket upgrade; a plain HTTP request to `/v1/realtime` is not served. ## Errors Realtime errors are delivered as a JSON frame (`{ "type": "error", "error": { "code", "message" }, "request_id": "..." }`) immediately before the WebSocket is closed. Use `error.code` for programmatic handling — it is the stable, semantic identifier. The WebSocket close code that follows is incidental. ### `error.code` reference | `error.code` | Meaning | | - | - | | `invalid_api_key` | Missing, malformed, or revoked `rsk_...` key. | | `insufficient_quota` | Balance can't cover the session hold, or ran out mid-session. Top up to reconnect. | | `rate_limit_exceeded` | An organization, team or member rate limit was hit. | | `spend_limit_exceeded` | An organization, team or member spend cap was reached. | | `concurrent_sessions_exceeded` | Too many open sessions on the account, or the server is at capacity. Close one or retry. | | `session_token_cap_exceeded` | Session exceeded its configured token cap. | | `session_duration_exceeded` | Session reached the 30-minute limit. Reconnect. | | `idle_timeout` | No client frame for 60 seconds. | | `connection_timeout` | The heartbeat was lost. Reconnect. | | `service_draining` | The server is restarting. Reconnect. | | `upstream_overload` | The upstream queue overflowed. Slow down or retry. | | `feature_not_enabled` | Realtime is not enabled on this deployment. | | `model_not_found` | The requested `model` is not available to your account or does not support realtime. | | `model_excluded` / `model_brand_unknown` | Your key's model policy does not allow this model. | | `model_not_available_on_pinned_provider` | Your key is pinned to a provider that cannot serve this model. | | `provider_error` | The upstream provider returned an error. Inspect `error.message` for detail. | In-band errors during a live session arrive as a regular `error` event with the OpenAI-shaped envelope shown above and are not accompanied by a socket close. ### WebSocket close codes (incidental) These close codes accompany the JSON error frame on session-terminating errors. Check `error.code` for the semantic reason; the close code below is informational only. | Close code | When you'll see it | | - | - | | `1000` | Normal closure, including auth, quota, model, limit and lifecycle errors. | | `1011` | Server-side or upstream condition: `provider_error`, `upstream_overload`, `service_draining`, `connection_timeout`, `feature_not_enabled`. | ## Next steps * For the ElevenLabs Scribe realtime STT frame protocol, see [Speech to Text → WebSocket Real-Time STT](/docs/capabilities/speech-to-text#websocket-real-time-stt). * Symptom-by-symptom fixes live in [Troubleshooting → Realtime Audio](/debug/realtime-audio). * Review the [Authentication guide](/docs/getting-started/authentication) for key rotation and scoping. * See the [API reference entry](/docs/reference/api-overview) for the OpenAPI stub. * Watch the upstream [OpenAI Realtime API reference](https://platform.openai.com/docs/api-reference/realtime) for new event types — Mesh forwards them without code changes. # Responses API Source: https://developers.meshapi.ai/docs/capabilities/responses-api Reasoning effort, hosted server-side tools, and background jobs — the /v1/responses endpoint and how it differs from chat completions. `/v1/responses` is an OpenAI-compatible alternative to chat completions, aimed at reasoning models and **hosted tools** the provider runs on your behalf. Use it when you need controllable reasoning effort, provider-hosted capabilities like web search or code execution, or long-running work that outlives a single HTTP connection. For ordinary chat, `/v1/chat/completions` remains the right endpoint. *** ## Reasoning effort Reasoning models can be told how much thinking to spend: ```bash theme={null} curl https://api.meshapi.ai/v1/responses \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "openai/o3", "input": "Prove that the square root of 2 is irrational.", "reasoning": { "effort": "high" } }' ``` Higher effort produces more reasoning tokens — better results on hard problems, at higher cost and latency. Reasoning tokens are billed. Chat completions accepts the same nested `reasoning` object (`effort`, or a `max_tokens` thinking budget). It also still accepts the older flat `reasoning_effort` field; when both are sent, the `reasoning` object wins. *** ## Hosted tools Unlike [function calling](/docs/capabilities/tool-calling), where *you* execute the function, hosted tools run on the provider's side and return their results directly. Hosted tools need a model with native Responses API support; sending one to any other model returns `400 model_capability_not_supported`. Function tools work on every model. | Tool | What it does | | - | - | | `web_search_preview` | Searches the web and grounds the answer | | `code_interpreter` | Executes code in a sandbox | | `file_search` | Searches files you've made available | | `image_generation` | Generates images inline | | `computer_use_preview` | Drives a virtual computer | ```json theme={null} { "model": "openai/gpt-4o", "input": "Summarize this week's news about AI regulation.", "tools": [{ "type": "web_search_preview" }] } ``` **`web_search_preview` forces the request into background mode.** Web search is slow enough that a dropped HTTP connection would lose the billing record, so MeshAPI upgrades these requests to `background: true` automatically. The practical consequence: you get back a **queued job**, not a completed answer, even though you didn't ask for one. Code that expects a synchronous result will break. Poll `GET /v1/responses/{id}` for the outcome. Models that only run as background jobs are upgraded the same way. *** ## Background jobs Set `background: true` for work that shouldn't be tied to a live connection: ```json theme={null} { "model": "openai/o3", "input": "...", "background": true } ``` You receive a response id immediately, then poll: ```bash theme={null} curl https://api.meshapi.ai/v1/responses/ \ -H "Authorization: Bearer " ``` `GET /v1/responses` lists your jobs. Background mode needs a model with native Responses API support (otherwise `400`), and cannot be combined with `service_tier: "flex"`. You can have at most **10 active background jobs** at once; jobs in a terminal state don't count, and an 11th returns `429 job_limit_exceeded`. Credit is reserved when a job is queued and settled when it finishes, so a queued job reduces your spendable balance before it produces anything. A balance too low to reserve returns `402 insufficient_balance`. *** ## Provider-specific fields Several fields are honoured only on particular upstreams — `thinking`, `caching`, `store`, `expire_at`, and `context_management` among them. An unsupported field is **silently ignored**, not rejected. Sending `store: true` to a provider that doesn't implement it returns a perfectly normal response in which nothing was stored. Verify behaviour on the specific model you are targeting rather than assuming a field took effect. Other supported controls include `previous_response_id` for continuing a conversation, `max_tool_calls` (1–10) to bound hosted tool usage, `include` for requesting extra output, and `text.format` for structured output. *** ## Related * [Tool Calling](/docs/capabilities/tool-calling) — functions your own code executes * [Web Search](/docs/capabilities/web-search) — the standalone search endpoint, billed differently * [Structured Output](/docs/capabilities/structured-output) # Routing Algorithms Source: https://developers.meshapi.ai/docs/capabilities/routing-algorithms How the Auto Router picks a model: the routing algorithms, the scoring formula, and the weights you can tune. [Auto Routing](/docs/capabilities/auto-routing) covers how to *use* `model: "auto"`. This page covers how the pick is actually made, and the one knob you can turn. ## Two layers A routing decision goes through two layers, and the word "weight" means something different in each: 1. **Which algorithm chain runs** — traffic is split across named *paths* by **traffic weights**. Operator-configured; you observe the result, you don't set it. 2. **Which model that algorithm picks** — the `weighted` algorithm scores every candidate with **scoring weights** over quality, cost, and latency. You *can* influence this per request, with [`weight_profile`](#choosing-a-profile). ## The algorithms Each path is an **ordered waterfall**. Algorithms run in sequence and the first one that returns a model wins; an algorithm that declines ("abstains") costs nothing and falls through to the next. Every algorithm has its own timeout, and none of them can fail your request — see [Nothing here can fail a request](#nothing-here-can-fail-a-request). | Algorithm | How it picks | LLM call | Typical use | | - | - | - | - | | `heuristic` | String-gates trivial prompts (greetings, one-line questions) and routes straight to a configured fast-lane model | No | A fast lane in front of the others, with no classify round-trip | | `weighted` | Classifies, then scores the candidate pool on quality, cost and latency and takes the argmax | Yes (classifier) | The main selector | | `benchmark` | Classifies into a task category, then takes the top-ranked model for that category | Yes (classifier) | Quality-led picking; also the `weighted` fallback | | `registry` | Shows the full candidate list to a classifier, which picks by reading model descriptions | Yes (classifier) | Last resort when the category ranking has nothing | | `not_diamond` | Sends the prompt to Not Diamond's trained router, which picks among the candidates it knows | Yes (external router) | An optional leg; abstains when fewer than two candidates are known to it | Which algorithms run, and in what order, is set by the active configuration. See [Traffic weights](#traffic-weights). `heuristic` is deliberately conservative. Any hint of task work, recency, code, links, more than six digits, more than one line or length over 140 characters makes it decline. So does a reference to an attachment the fast-lane model can't read, or a prompt that is neither a question nor small talk. The request then takes the full path instead. Over-declining is harmless; a wrong fast-lane pick is not. ### What the pool is Every algorithm picks from the same **candidate pool**: the models the gateway currently serves for that surface, already filtered by your key's model policy and the request's capability requirements. An algorithm can only ever return a model you were entitled to call directly. ## Traffic weights The active configuration names one or more paths, each with a relative weight, and a path is chosen per request by weighted random. ```json theme={null} [ { "name": "weighted-first", "weight": 90, "algorithms": [ { "name": "heuristic", "timeout_ms": 200 }, { "name": "weighted", "timeout_ms": 5000 }, { "name": "benchmark", "timeout_ms": 5000 }, { "name": "registry", "timeout_ms": 5000 } ] }, { "name": "benchmark-first", "weight": 10, "algorithms": [ { "name": "heuristic", "timeout_ms": 200 }, { "name": "benchmark", "timeout_ms": 5000 }, { "name": "registry", "timeout_ms": 5000 } ] } ] ``` Weights are **relative positive integers**, normalised internally — `[90, 10]` and `[9, 1]` are identical. The shape above is illustrative; the live split is an operating decision and changes without an API change. ## Scoring weights When the `weighted` algorithm runs, it scores every candidate in the pool: ``` score(m) = w_q · Q(m) + w_c · C(m) + w_l · L(m) ``` Highest score wins. The runners-up become the fallover chain, so if the winner fails the request moves down the same ranking rather than to an unrelated model. ### The three signals All three are normalised so **higher is always better**, which is why cost and latency are inverted — a cheap model scores high on `C`, a fast one scores high on `L`. | Signal | Source | Normalisation | | - | - | - | | **Q** — quality | The model's position in the ranking for the classified task category. Candidates with a measured benchmark index (coding / math / general, chosen by category) are reordered within the slots their cohort already holds — best index first, ties broken on curated rank | `1 - pos/(N-1)` over the pool | | **C** — cost | Blended list price per 1M tokens: `0.3 × prompt + 0.7 × completion` | Log-scaled, then min-max inverted | | **L** — latency | Observed p95 for that model — **time-to-first-token** on streaming requests, full wall-clock otherwise | Min-max inverted | Only the *order* of a measured index matters, never its magnitude, and a candidate with no measured score keeps its curated position rather than dropping. Nothing falls out of the pool for want of a benchmark. ### Missing signals degrade, they don't error A candidate missing a signal drops that term, and its remaining weights are renormalised to sum to 1. A candidate with neither cost nor latency data is scored on quality alone — so when the whole pool has no cost or latency data, `weighted` returns exactly what `benchmark` would have. ## Weight profiles The `(w_q, w_c, w_l)` vector is named. Four profiles ship: | Profile | w\_q (quality) | w\_c (cost) | w\_l (latency) | Picks | | - | - | - | - | - | | `quality_first` | 0.75 | 0.15 | 0.10 | The strongest model that isn't wildly expensive | | `balanced` *(default)* | 0.40 | 0.35 | 0.25 | The knee of the quality/cost curve | | `cost_first` | 0.15 | 0.70 | 0.15 | The cheapest model that clears a quality floor | | `latency_first` | 0.15 | 0.25 | 0.60 | The fastest model that clears a quality floor | Because each candidate's present weights are renormalised, only the **ratio** between `w_q`, `w_c` and `w_l` affects the outcome, not the absolute sum. The table shows the shipped vectors; the gateway operator can adjust a profile's vector without an API change. ## Choosing a profile Send `weight_profile` on a chat completions request: ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "auto", "weight_profile": "cost_first", "messages": [{"role": "user", "content": "Summarise this changelog in one line"}] }' ``` ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -H "X-Mesh-Weight-Profile: latency_first" \ -H "Content-Type: application/json" \ -d '{ "model": "auto", "messages": [{"role": "user", "content": "Summarise this changelog in one line"}] }' ``` The header exists for proxies and gateways that cannot rewrite a JSON body. When both are sent, **the body field wins** — it is the parameter you wrote, the header is something an intermediary may have injected. ```python theme={null} import httpx response = httpx.post( "https://api.meshapi.ai/v1/chat/completions", headers={"Authorization": "Bearer rsk_YOUR_KEY"}, json={ "model": "auto", "weight_profile": "cost_first", "messages": [{"role": "user", "content": "Summarise this changelog in one line"}], }, ) ``` The Python SDK's `ChatCompletionParams` has no `weight_profile` field and rejects unknown fields, so send the request directly as above, or set the profile on the key. ### Precedence The effective profile is resolved highest-first: 1. **Request** — the `weight_profile` body field, or the `X-Mesh-Weight-Profile` header 2. **API key** — the `weight_profile` entry in the key's routing policy 3. **Team / organization** — the same entry on the key's routing-policy template, else your org's default template 4. **Gateway default** — `balanced` So you can set a house default on the key and still override it for one request. **An unrecognised profile name on a request is not an error.** A typo like `"cheapest"` in the body or header falls back to `balanced` and the request is served normally. A misspelt profile is silently the default rather than a 400. Check the spelling against the four names above if routing isn't behaving the way you expect. On a key's routing policy or a template, an unknown name is rejected with a `422` when you save it. `weight_profile` applies to **`POST /v1/chat/completions` only**, and only while `weighted` is the algorithm serving the request. On `/v1/responses` and `/v1/router/select` it is ignored and the gateway default applies. Sending it is always safe — it is stripped before your request reaches the upstream provider. ## Previewing a decision `POST /v1/router/select` returns the model the Auto Router *would* pick, without running inference or billing you for one. Useful for pinning a model yourself, or for checking what a prompt classifies as. ```bash theme={null} curl https://api.meshapi.ai/v1/router/select \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "messages": [{"role": "user", "content": "Write a Python quicksort implementation"}] }' ``` ```json theme={null} { "model": "anthropic/claude-haiku-4.5", "auto_router": { "fallback_used": false, "fallback_reason": null, "router_version": "v10" }, "reasoning_effort": "medium" } ``` The routing-data version that made the pick is also returned in the `X-Mesh-Router-Version` header. The key needs the `router:read` scope. Optional request fields: | Field | Effect | | - | - | | `candidate_models` | Restricts the pick to ids you name, intersected with your key's policy. An empty list or empty intersection is a `422` | | `exclude_models` | Removes ids from consideration, which is how an "ask another model" flow avoids re-picking the one already shown. Excluding every remaining model is a `422` | | `api_type` | `completions` (default) or `responses`: which surface's pool to pick from | | `router_version` | The routing-data version to pick on. See [Auto Routing](/docs/capabilities/auto-routing#routing-data-version) | | `tags` | Your own labels for the request, as on inference requests | The classifier still runs, so a select call takes roughly as long as the routing stage of a real request. `reasoning_effort` is a hint: the `benchmark` and `weighted` algorithms set it from the classification, `heuristic` returns `"none"`, and it is `null` otherwise. ## Nothing here can fail a request `model: "auto"` always resolves to a concrete model. Every layer is fail-soft by design: * An algorithm that declines, times out, or errors falls through to the next in the chain. * A chain that exhausts every algorithm falls through to the configured default model. * A missing scoring signal drops that term instead of dropping the candidate. * An unknown `weight_profile` on a request degrades to `balanced`. * A settings-store outage falls back to the built-in defaults rather than refusing to route. Whenever the default model serves, or the router's pick had to be replaced, the response says so: `x_auto_routed_fallback` and `x_auto_routed_fallback_reason` on the body, or `X-Auto-Routed-Fallback` and `X-Auto-Routed-Fallback-Reason` on a stream. A pick made by a later algorithm in the chain is not a fallback. See [Fallback behavior](/docs/capabilities/auto-routing#fallback-behavior) for the reasons, and [Debug → Auto Routing](/debug/auto-routing) when a pick surprises you. # Service Tiers Source: https://developers.meshapi.ai/docs/capabilities/service-tiers Trade latency for a lower token price with service_tier: flex on OpenAI models — which models serve it, what it costs, and how the refusals behave. `service_tier` is an optional field on `POST /v1/chat/completions` and `POST /v1/responses`. It selects the capacity pool the provider serves your request from. | Value | What it does | | - | - | | `"auto"` | Inert. Forwarded unchanged — no change in price, latency or behaviour. | | `"default"` | Inert. Same as `"auto"`. | | `"flex"` | Serves the request from OpenAI's discounted, slower capacity pool and bills it at OpenAI's flex rates. | Omitting the field is the same as sending `"default"`. Those three are the **only** accepted values. `"priority"`, `"fast"` and `"scale"` are OpenAI's *more expensive* tiers and are not supported — they are rejected with `422` before the request reaches a provider: ```json theme={null} { "error": { "code": "validation_error", "message": "Request validation failed.", "details": [ { "type": "literal_error", "loc": ["body", "service_tier"], "msg": "Input should be 'auto', 'default' or 'flex'", "input": "priority", "ctx": { "expected": "'auto', 'default' or 'flex'" } } ] }, "request_id": "req_01JQ..." } ``` *** ## Which models serve flex Flex is an OpenAI feature. It is refused on every other provider, whatever model you name. Within OpenAI, only the models OpenAI publishes a complete flex rate card for can serve it. That set changes as OpenAI's pricing moves, so read it from `GET /v1/models` rather than hardcoding a list: * `pricing.flex_input_usd_per_1m` — **this is the signal to branch on.** A flex rate is published only for a model whose provider can actually serve the tier, so its presence is the closest thing to a yes. * `supports_flex` — the model's own declaration, and weaker than it looks. It does not account for which upstream serves the model, so a row reached through a provider with no flex tier still reports `true`. ```bash theme={null} curl -s https://api.meshapi.ai/v1/models \ -H "Authorization: Bearer " \ | jq -r '.[] | select(.pricing.flex_input_usd_per_1m != null) | .id' ``` Neither field is a guarantee, for two reasons. `GET /v1/models` publishes a model's **default** pricing row, while the tier is checked against the row for the provider your request actually resolves to — so a key with its own provider credential, a key pinned to a `fixed_provider`, or a brand routing rule can land on a row with no flex tier. And a model can acquire a rate that flex does not price after its flex prices were published. Treat a published flex rate as "expected to work" and handle the `400` below. ## What it costs Flex tokens bill at OpenAI's published flex rates, materially below the standard rates. The rates are transcribed from OpenAI's card per model rather than derived from a discount, so there is no ratio to apply — the per-model `pricing.flex_*` values in `GET /v1/models` are the contract. Prompt caching still applies on a flex request and bills at the model's flex cache rates. A flex request is **never** billed at a standard rate: if any rate the model carries has no flex counterpart, the request is refused rather than priced off the standard card. Flex is not the same product as the [Batch API](/docs/capabilities/batch-api), which is asynchronous and priced off its own rate card. ## Sending a flex request ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-5-mini", "messages": [{ "role": "user", "content": "Summarise this changelog." }], "service_tier": "flex" }' ``` The same field works identically on `POST /v1/responses`. ## A model that cannot serve flex is refused A flex request for a model that cannot serve the tier is rejected with `400` `model_capability_not_supported` before anything is sent upstream. It is **never** silently downgraded to the standard tier and billed at the standard price. Two messages, two causes. The provider your request resolved to has no flex tier, or the model publishes no flex input/output rate for the context length it would use: ```json theme={null} { "error": { "code": "model_capability_not_supported", "message": "Model 'openai/gpt-4o-mini' is not available on the 'flex' service tier from the provider resolved for this request. Omit service_tier, or use a model that publishes flex pricing." }, "request_id": "req_01JQ..." } ``` Or the headline rates are there but something else on the card is not — a cache, reasoning or audio rate the model charges that flex publishes no counterpart for. This one can appear on a model that served flex yesterday, so handle it rather than treating a published flex rate as permanent: ```json theme={null} { "error": { "code": "model_capability_not_supported", "message": "Model 'openai/gpt-5-mini' is not priced for the flex service tier. Retry without `service_tier`." }, "request_id": "req_01JQ..." } ``` Both remedies are the same: drop `service_tier`, or pick a model that publishes a flex rate. ## Knowing which tier actually served A request can end up on the standard tier even though you asked for flex. If [retry and fallback](/docs/platform/retry-and-fallback) moves the request to a different provider or model that cannot serve flex, the tier is dropped for that attempt and the request is served at the standard tier — and billed at that target's standard rate, not at the flex rate you asked for. Two ways to see what happened: | Surface | When to use it | | - | - | | `X-Mesh-Service-Tier` response header | Non-streaming responses. Present only when flex served; absent means standard. | | The request's usage record | **Streaming responses**, where the headers are already sent before the attempt finishes, so there is no header to read. `service_tier` on the record is the authority. | ## Caching is per tier The [gateway response cache](/docs/capabilities/caching) keys on the service tier, so a flex request never replays a response that was served at the standard tier, and vice versa. Two otherwise-identical requests on different tiers are two cache entries. ## Flex is slower by design That is the trade you are making. Raise your client's timeout before you use it — OpenAI recommends allowing up to 15 minutes for a flex request. The per-request `timeout` field is capped at **600 seconds**, on every tier. A flex request that needs longer than that fails with `504 gateway_timeout`, which is expected under load — retry it, or send the request without `service_tier`. ## When OpenAI's flex capacity is full Flex runs on spare capacity, so it can be unavailable when the standard tier is fine. OpenAI does not charge for a request refused this way, and neither do we: ```json theme={null} { "error": { "code": "upstream_error", "message": "Service tier is at capacity. Please try again later.", "provider_code": "provider_overloaded", "provider_error": { "provider": "openai", "status": 429, "message": "Service tier is at capacity. Please try again later." } }, "request_id": "req_01JQ..." } ``` It is served as `503` with a `Retry-After` header. Mesh treats it as a capacity condition rather than a rate limit, so the request is retryable and eligible for fallback to another model. Two remedies: * Retry with exponential backoff, honouring `Retry-After`. * Retry without `service_tier` (or with `"auto"`) to take standard capacity at the standard price. See the [Error Reference](/docs/reference/errors) for the full error contract. ## Where flex is not available These are deliberate, not gaps: | Surface | Why | | - | - | | [`/v1/batches`](/docs/capabilities/batch-api) | Batch is already a discounted asynchronous product with its own rate card. A `service_tier` on a batch line is **not** read by Mesh — it is neither validated nor priced, so it buys you nothing. | | [Video generation](/docs/capabilities/video-generation) | Video requests have a `service_tier` field of their own. It is an unrelated BytePlus parameter and has nothing to do with OpenAI's tiers. | | `"background": true` on `/v1/responses` | A background job settles outside the request, where the flex rate card cannot be applied. Combining the two is refused with `400`. | | [Realtime WebSocket endpoints](/docs/capabilities/realtime-audio) | No tier selection. | The background refusal looks like this: ```json theme={null} { "error": { "code": "model_capability_not_supported", "message": "background=true cannot be served on the 'flex' service tier. Omit service_tier, or run the request without background." }, "request_id": "req_01JQ..." } ``` ## Tracking flex spend `service_tier` is recorded on every usage record and is filterable in usage analytics, so flex spend can be separated from standard spend without tagging requests yourself. See the [Usage API](/docs/reference/usage-api). # Speech to Text Source: https://developers.meshapi.ai/docs/capabilities/speech-to-text Text-to-speech, speech-to-text, voice management, and real-time streaming audio APIs. # Audio Generation Mesh API provides a full suite of audio endpoints — convert text to speech, transcribe audio files, stream TTS/STT in real time, and browse available voices — all through a single API key. All endpoints share the same base URL: `https://api.meshapi.ai/v1/audio` **Auth:** `Authorization: Bearer rsk_` on all REST requests. WebSocket endpoints accept the key via `Sec-WebSocket-Protocol: Bearer ` or `?api_key=`. *** ## Speech-to-Text `POST /v1/audio/transcriptions` Transcribe an audio file. The provider is resolved automatically from the model name. You can supply audio as a file upload, a public URL, or a cloud storage URL. This endpoint uses `multipart/form-data` — not JSON. Multiple STT providers are supported, e.g. `elevenlabs/scribe_v2`, `sarvam/saaras:v3`, `openai/whisper-large-v3`, `google/gemini-3.5-transcribe` and `gpt-4o-transcribe`. There is no default: `model` is required. To list transcription-capable models, use the search endpoint filtered by modality: `GET /v1/models/search?input_modality=audio&output_modality=text`. ### Form fields | Field | Type | Description | | :- | :- | :- | | `model` | string | **Required.** Model ID (e.g. `elevenlabs/scribe_v2`, `sarvam/saaras:v3`). | | `response_format` | string | `json` (default) returns `{ "text": ... }`; `verbose_json` adds per-word detail. See [Response](#response). | | `file` | file | Audio file to transcribe. Required unless `source_url` or `cloud_storage_url` is provided. | | `source_url` | string | Public URL to an audio file. Alternative to uploading a file. | | `cloud_storage_url` | string | Cloud storage URL (S3, GCS). Alternative to uploading a file. | | `language_code` | string | BCP-47 language code (e.g. `en-US`). Leave blank for auto-detection. | | `diarize` | boolean | Identify and label different speakers. | | `num_speakers` | integer | Expected number of speakers (1–32). Helps diarization accuracy. | | `timestamps_granularity` | string | `word` or `character` — granularity of returned timestamps. | | `tag_audio_events` | boolean | Detect non-speech events like laughter, applause, etc. | | `diarization_threshold` | float | Speaker change sensitivity (0.1–0.4). | | `additional_formats` | string | JSON-encoded list of extra output formats (ElevenLabs-specific). | | `file_format` | string | Explicit audio format hint if the file extension is ambiguous. | | `temperature` | float | Sampling temperature (0–2). | | `seed` | integer | Reproducible seed (0–2147483647). | | `use_multi_channel` | boolean | Process each audio channel independently. | | `webhook` | boolean | Return the result via webhook instead of inline. | | `webhook_id` | string | ID of a pre-configured ElevenLabs webhook. | | `webhook_metadata` | string | Custom metadata string to include with the webhook callback. | | `keyterms` | string\[] | Domain-specific terms to boost recognition accuracy. | | `entity_detection` | string | Enable named entity detection. | | `entity_redaction` | string | Redact specific entity types from the transcript. | | `entity_redaction_mode` | string | How redacted entities are shown: `replace` or `remove`. | | `no_verbatim` | boolean | Clean up filler words and disfluencies. | | `detect_speaker_roles` | boolean | Classify speakers by role (e.g. interviewer vs. interviewee). | | `with_timestamps` | boolean | Sarvam-specific: include word-level timestamps. | | `debug_mode` | boolean | Sarvam-specific: return extra debug information. | | `tags` | string | Your own labels, as `k=v,k2=v2` or a JSON object, up to 16 pairs. Filter and group spend by them in usage reports. | | `safety_identifier` | string | A stable, anonymised id for the end user the request was made for (a hash, not an email), so spend can be attributed per user. | `enable_logging` is a query parameter, not a form field: `POST /v1/audio/transcriptions?enable_logging=false` opts out of ElevenLabs request logging (default `true`). Sent as a form field it is ignored and logging stays on. ### Response ```json theme={null} { "text": "The transcribed text goes here." } ``` With `response_format=verbose_json`: ```json theme={null} { "text": "Hello there. (laughter)", "language_code": "en", "language_probability": 0.98, "words": [ { "text": "Hello", "start": 0.0, "end": 0.42, "type": "word", "speaker_id": "speaker_0" }, { "text": "(laughter)", "start": 1.1, "end": 1.6, "type": "audio_event", "speaker_id": null } ] } ``` `type` separates speech (`word`, `spacing`) from non-speech `audio_event` tokens, so you can drop events without parsing the text. ### Examples ```bash theme={null} curl -X POST https://api.meshapi.ai/v1/audio/transcriptions \ -H "Authorization: Bearer rsk_..." \ -F "model=elevenlabs/scribe_v2" \ -F "file=@recording.mp3" \ -F "language_code=en-US" \ -F "diarize=true" ``` ```bash theme={null} curl -X POST https://api.meshapi.ai/v1/audio/transcriptions \ -H "Authorization: Bearer rsk_..." \ -F "model=elevenlabs/scribe_v2" \ -F "source_url=https://example.com/meeting.mp3" \ -F "diarize=true" \ -F "num_speakers=3" ``` ```python theme={null} import httpx with open("recording.mp3", "rb") as f: response = httpx.post( "https://api.meshapi.ai/v1/audio/transcriptions", headers={"Authorization": "Bearer rsk_..."}, data={ "model": "elevenlabs/scribe_v2", "language_code": "en-US", "diarize": "true", }, files={"file": ("recording.mp3", f, "audio/mpeg")}, ) print(response.json()["text"]) ``` ```ts theme={null} import fs from "fs"; const form = new FormData(); form.append("model", "elevenlabs/scribe_v2"); form.append("language_code", "en-US"); form.append("file", new Blob([fs.readFileSync("recording.mp3")]), "recording.mp3"); const res = await fetch("https://api.meshapi.ai/v1/audio/transcriptions", { method: "POST", headers: { Authorization: "Bearer rsk_..." }, body: form, }); const { text } = await res.json(); console.log(text); ``` *** ## Transcribe and Translate `POST /v1/audio/transcriptions/translate` Transcribe audio and translate the result directly to English in a single step. This is a legacy alias of [`POST /v1/audio/translations`](/docs/capabilities/audio#translate-audio-to-english), with the same behaviour. Prefer that path, which also takes `response_format` and `temperature`. This endpoint uses `multipart/form-data`. ### Form fields | Field | Type | Description | | :- | :- | :- | | `model` | string | Model to use. Must support translation. Always pass it. | | `file` | file | Audio file. **Required.** | | `prompt` | string | Optional context prompt to guide translation. | | `tags` | string | Optional comma-separated labels for usage reporting. | | `safety_identifier` | string | Optional stable, anonymised end-user id (a hash, not an email). | ### Response ```json theme={null} { "text": "The English translation of the transcribed audio." } ``` ### Example ```bash theme={null} curl -X POST https://api.meshapi.ai/v1/audio/transcriptions/translate \ -H "Authorization: Bearer rsk_..." \ -F "model=openai/whisper-large-v3" \ -F "file=@hindi_recording.wav" ``` If the selected model doesn't support translation, the API returns a `422` error. Check `GET /v1/models` to confirm a model's capabilities. *** ## WebSocket Real-Time STT `WS /v1/audio/transcriptions/realtime` Transcribe audio in real time. Send raw audio chunks as they are captured (e.g. from a microphone) and receive partial and final transcripts as they are produced. The wire protocol is selected automatically from the model you pass. Standard streaming models (e.g. `openai/whisper-large-v3`) use an OpenAI-compatible frame protocol. ElevenLabs models (`elevenlabs/scribe_v2_realtime`) use ElevenLabs' Scribe v2 realtime frames — see the [ElevenLabs models](#elevenlabs-models) subsection below. ### Authentication Pass your Mesh API key in one of these ways: * `Sec-WebSocket-Protocol: Bearer rsk_...` header * `?api_key=rsk_...` query parameter * `?token=rsk_...` query parameter ### Query parameters | Parameter | Default | Description | | :- | :- | :- | | `model` | — | **Required.** Model ID (e.g. `openai/whisper-large-v3`, `elevenlabs/scribe_v2_realtime`). | | `audio_format` | `pcm_16000` | Format of the audio chunks you will send. | | `language_code` | — | BCP-47 language code. Omit for auto-detection. | | `commit_strategy` | `manual` | `manual` — you commit explicitly. `auto` — VAD-driven commits. | | `include_timestamps` | `false` | Include word-level timestamps in transcripts. | | `include_language_detection` | `false` | Include detected language in each transcript event. | | `keyterms` | — | Domain-specific terms to boost recognition. | | `no_verbatim` | `false` | Remove filler words and disfluencies. | | `vad_silence_threshold_secs` | — | Silence duration before VAD triggers a commit (0.3–3s). | | `vad_threshold` | — | VAD speech probability threshold (0.1–0.9). | | `min_speech_duration_ms` | — | Minimum speech segment length (50–2000 ms). | | `min_silence_duration_ms` | — | Minimum silence to trigger a split (50–2000 ms). | | `enable_logging` | `true` | Pass `false` to opt out of request logging (ElevenLabs models). | Each streaming session reserves \$10 of your balance while open, and an account can hold 5 concurrent WebSocket sessions, shared with [realtime](/docs/capabilities/realtime-audio) and streaming TTS sessions. **Supported audio formats:** `pcm_8000`, `pcm_16000`, `pcm_22050`, `pcm_24000`, `pcm_44100`, `pcm_48000`, `ulaw_8000` ### Standard streaming models Standard streaming models — e.g. `openai/whisper-large-v3` — use an OpenAI-compatible frame protocol. **Client → server (JSON frames)** | Frame type | Payload | Description | | :- | :- | :- | | `input_audio_buffer.append` | `{ "audio": "" }` | Append a chunk of audio. `audio` is base64-encoded PCM `s16le`, 16 kHz, mono. | | `input_audio_buffer.commit` | `{}` | Commit the buffered audio and request a final transcript. | ```json theme={null} { "type": "input_audio_buffer.append", "audio": "" } ``` **Server → client (JSON frames)** | Frame type | Payload | Description | | :- | :- | :- | | `conversation.item.input_audio_transcription.delta` | `{ "delta": "..." }` | Interim transcript chunk — text may still change. | | `conversation.item.input_audio_transcription.completed` | `{ "transcript": "..." }` | Final transcript for the committed audio segment. | ### Example ```js theme={null} const ws = new WebSocket( "wss://api.meshapi.ai/v1/audio/transcriptions/realtime" + "?model=openai/whisper-large-v3&api_key=rsk_..." ); ws.onopen = () => { console.log("Connected. Start sending audio chunks."); }; ws.onmessage = ({ data }) => { const frame = JSON.parse(data); if (frame.type === "conversation.item.input_audio_transcription.delta") { process.stdout.write("\r" + frame.delta); } else if ( frame.type === "conversation.item.input_audio_transcription.completed" ) { console.log("\nFinal:", frame.transcript); } }; // Send a PCM audio chunk (s16le, 16 kHz, mono — from a microphone, for example) function sendAudioChunk(pcmBuffer) { const b64 = Buffer.from(pcmBuffer).toString("base64"); ws.send( JSON.stringify({ type: "input_audio_buffer.append", audio: b64, }) ); } // Commit the buffered audio to get a final transcript function commit() { ws.send(JSON.stringify({ type: "input_audio_buffer.commit" })); } ``` ### ElevenLabs models ElevenLabs models (`elevenlabs/scribe_v2_realtime`) use ElevenLabs' Scribe v2 realtime frames. **Client → server (JSON frames)** Send `input_audio_chunk` frames with base64-encoded audio. This is the only message type the server forwards upstream — any other message type is silently dropped. ```json theme={null} { "message_type": "input_audio_chunk", "audio_base_64": "", "sample_rate": 16000, "commit": false } ``` Set `"commit": true` to trigger a VAD commit when using `commit_strategy: manual`. **Server → client (JSON frames)** | Frame type | Description | | :- | :- | | `session_started` | Sent once after connection is established. | | `partial_transcript` | In-progress transcript — text may still change. | | `committed_transcript` | Final transcript for the committed audio segment. | | `committed_transcript_with_timestamps` | Final transcript including word-level timing data. | | `error` | Sent before closing on auth or upstream errors. | ```js theme={null} const ws = new WebSocket( "wss://api.meshapi.ai/v1/audio/transcriptions/realtime" + "?model=elevenlabs/scribe_v2_realtime&audio_format=pcm_16000&commit_strategy=manual&api_key=rsk_..." ); ws.onopen = () => { console.log("Connected. Start sending audio chunks."); }; ws.onmessage = ({ data }) => { const frame = JSON.parse(data); if (frame.message_type === "partial_transcript") { process.stdout.write("\r" + frame.text); } else if (frame.message_type === "committed_transcript") { console.log("\nFinal:", frame.text); } }; // Send a PCM audio chunk (from a microphone, for example) function sendAudioChunk(pcmBuffer) { const b64 = Buffer.from(pcmBuffer).toString("base64"); ws.send( JSON.stringify({ message_type: "input_audio_chunk", audio_base_64: b64, sample_rate: 16000, }) ); } // Commit the buffered audio to get a final transcript function commit() { ws.send( JSON.stringify({ message_type: "input_audio_chunk", audio_base_64: "", sample_rate: 16000, commit: true, }) ); } ``` *** ## Voice Management ### List voices `GET /v1/audio/voices` Returns a unified voice catalog spanning every TTS model brand. The list can be large, so **filter by `brand` or `model`** to narrow it to the voices you can actually use with the model you plan to call. | Query parameter | Description | | :- | :- | | `brand` | Filter by model brand — e.g. `elevenlabs`, `hexgrad`, `canopylabs`. This is the primary way to scope the catalog. | | `model` | Filter by a specific model id (e.g. `hexgrad/kokoro-82m`). ElevenLabs voices carry `model: null`, so a `model` filter alone drops them; filter those with `brand=elevenlabs`. | | `search` | Substring match on voice id / name. | Each voice is returned with `voice_id`, `name`, `brand`, `model`, `language`, `gender`, and `preview_url`. The response also carries `count` and a `truncated` flag (`true` when the list was capped before the full set was fetched). Pass a voice's `voice_id` as the `voice` field on `POST /v1/audio/speech` (or the `{voice_id}` path segment on the streaming endpoint) — and use the same `model` the voice is listed under. Filtering by `brand`/`model` keeps you from picking a voice that isn't valid for the model you call. ```bash theme={null} # All voices for a given brand curl "https://api.meshapi.ai/v1/audio/voices?brand=elevenlabs&search=rachel" \ -H "Authorization: Bearer rsk_..." # Voices usable with a specific model curl "https://api.meshapi.ai/v1/audio/voices?model=hexgrad/kokoro-82m" \ -H "Authorization: Bearer rsk_..." ``` ### Get a single voice `GET /v1/audio/voices/{voice_id}` Fetch metadata for a specific voice by its ElevenLabs voice ID. ```bash theme={null} curl https://api.meshapi.ai/v1/audio/voices/JBFqnCBsd6RMkjVDRZzb \ -H "Authorization: Bearer rsk_..." ``` *** ## Error handling All endpoints use standard HTTP status codes. Common cases: | Code | Meaning | | :- | :- | | `401` | Missing or invalid API key. | | `422` | Validation error — check the response body for details. | | `429` | Rate limit exceeded. | | `402` | Insufficient account balance. | WebSocket sessions send a JSON `error` frame and then close with code `1000` before disconnecting, except while the server is draining for a restart, when the close code is `1011`. Reconnect and retry. *** ## Available models | Model | Provider | Notes | | - | - | - | | `elevenlabs/scribe_v1` / `elevenlabs/scribe_v2` | ElevenLabs | High-accuracy multilingual STT with diarization and audio event tagging | | `elevenlabs/scribe_v2_realtime` | ElevenLabs | Real-time streaming STT (WebSocket) | | `sarvam/saaras:v3` | Sarvam AI | Optimised for Indian languages | | `google/gemini-3.5-transcribe` | Google | Gemini transcription | | `gpt-4o-transcribe` / `gpt-4o-mini-transcribe` | OpenAI | OpenAI transcription | | `openai/whisper-large-v3` | Together | Multilingual batch transcription | | `nvidia/parakeet-tdt-0.6b-v3` | Together | Fast, accurate English batch transcription | Together STT models support **batch** transcription (upload a file). Diarization, audio-event tagging, and `source_url`/`cloud_storage_url` are ElevenLabs-only features. For Indian-language audio, the `language_code` parameter significantly improves accuracy. Supported codes include `hi`, `ta`, `te`, `kn`, `ml`, `mr`, `gu`, `bn`. # Structured Output Source: https://developers.meshapi.ai/docs/capabilities/structured-output Constrain chat completion output to valid JSON or a strict JSON schema with response_format. The `response_format` field on `POST /v1/chat/completions` controls the shape of the model's output. Use it to get back machine-readable JSON instead of free-form prose — ideal for data extraction, classification, and tool-style workflows. ```http theme={null} POST https://api.meshapi.ai/v1/chat/completions Authorization: Bearer rsk_ ``` **Not every model enforces `response_format`.** It only takes effect on models that support structured output. On many models that don't, the request still **succeeds and returns ordinary text**; on others (DeepSeek, Arcee and ILMU, which implement only `json_object`) a `json_schema` request is rejected with a `4xx`. So if you get prose or an error back instead of the JSON you asked for, the model most likely doesn't support structured output: check its support on the [Models page](https://app.meshapi.ai) in your dashboard (**app.meshapi.ai → Models**, i.e. `https://app.meshapi.ai/org//models`) or the `supports_structured_output` flag returned by `GET /v1/models`, and switch to one with first-class support such as OpenAI models or Google Gemini (e.g. `google/gemini-2.5-flash`). See [Not getting JSON back?](#not-getting-json-back) and [How it works across providers](#how-it-works-across-providers). ## Output modes `response_format` is an object whose `type` selects the mode: | `type` | Behavior | | - | - | | `"text"` | Default. Free-form text — identical to omitting `response_format`. | | `"json_object"` | The model returns syntactically valid JSON. You describe the desired keys in your prompt; the structure is not otherwise enforced. | | `"json_schema"` | The model returns JSON that conforms to the JSON Schema you supply. The strongest option — field names and types are constrained regardless of the prompt. | In every mode the JSON is returned as a **string** in `choices[0].message.content`, which you parse client-side. ## Schema-enforced output (`json_schema`) Supply a `json_schema` object containing a `name` (a label) and a `schema` (the JSON Schema to enforce): ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer rsk_" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemini-2.5-flash", "messages": [ {"role": "user", "content": "What is the capital of France? Use the provided schema."} ], "response_format": { "type": "json_schema", "json_schema": { "name": "country_info", "schema": { "type": "object", "properties": { "country": {"type": "string"}, "capital": {"type": "string"} }, "required": ["country", "capital"], "additionalProperties": false } } } }' ``` The Python SDK's `chat.completions.parse()` takes a Pydantic model, a `TypedDict`/dataclass, or a raw schema `dict`, and returns a parsed, typed result — no manual schema dict or `json.loads()` needed. ```python theme={null} from pydantic import BaseModel from meshapi import ChatCompletionParams, ChatMessage, MeshAPI client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") class CountryInfo(BaseModel): country: str capital: str country = client.chat.completions.parse( ChatCompletionParams( model="google/gemini-2.5-flash", messages=[ ChatMessage(role="user", content="What is the capital of France?") ], temperature=0, ), response_format=CountryInfo, ) print(country.capital) # "Paris" — a typed CountryInfo instance ``` See [Structured Output](/sdk/structured-output) in the Python SDK reference for the `TypedDict`/raw-dict paths and the opt-in retry loop. ```ts theme={null} const schema = { type: "json_schema", json_schema: { name: "country_info", schema: { type: "object", properties: { country: { type: "string" }, capital: { type: "string" }, }, required: ["country", "capital"], additionalProperties: false, }, }, }; const resp = await client.chat.completions.create({ model: "google/gemini-2.5-flash", messages: [ { role: "user", content: "What is the capital of France? Use the provided schema." }, ], response_format: schema, temperature: 0, }); const data = JSON.parse(resp.choices[0].message?.content ?? "{}"); console.log(data.capital); // "Paris" ``` ## Valid JSON without a schema (`json_object`) When you only need parseable JSON and don't want to define a schema, use `json_object` and describe the keys you expect in the prompt: ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer rsk_" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemini-2.5-flash", "messages": [ {"role": "system", "content": "Respond only with JSON."}, {"role": "user", "content": "List three fruits and their colors with keys name and color."} ], "response_format": {"type": "json_object"} }' ``` The output is guaranteed to be valid JSON, but the exact keys depend on the model following your prompt. Use `json_schema` when you need that guarantee. ## How it works across providers `response_format` follows the OpenAI convention and is forwarded to the upstream provider. For providers with a different native contract, MeshAPI translates it automatically: * **Google Gemini on Vertex AI**: converted to Gemini's native structured-output config (`responseMimeType` for `json_object`, plus `responseSchema` for `json_schema`), so schema enforcement runs on the provider side. * **Amazon Bedrock** (including Anthropic Claude models served there): `json_schema` is converted to Bedrock's native structured-output config. Bedrock can't combine it with tool use, so on a request that also carries `tools`, `response_format` is dropped. `json_object` has no Bedrock equivalent, so describe the JSON in your prompt. * **DeepSeek, Arcee, ILMU**: `json_object` only; `json_schema` is rejected upstream with a `4xx`. Structured output works with any model that supports `response_format`. Elsewhere a model may return ordinary text or reject the request. ## Tips * Set `additionalProperties: false` in your schema to forbid extra fields. * Use `temperature: 0` for the most deterministic, schema-faithful output. * The content is a JSON string — parse it with `json.loads()` / `JSON.parse()`. * On success `finish_reason` is `"stop"`. ## Not getting JSON back? If the response comes back as ordinary prose instead of the JSON you asked for, the model you chose doesn't support structured output, or (on Bedrock) the request also carried `tools`. `response_format` is forwarded to the provider, but a provider that ignores it returns plain text. The request still succeeds with `finish_reason: "stop"`, so this fails **silently** rather than erroring. **Check that your model supports structured output.** Open the **Models** page in your dashboard — [app.meshapi.ai](https://app.meshapi.ai) → **Models** (`https://app.meshapi.ai/org//models`) — and confirm the model lists **Structured output** support, or read the `supports_structured_output` flag from `GET /v1/models`. Models with first-class support include OpenAI models and Google Gemini (e.g. `google/gemini-2.5-flash`). ## Common errors | HTTP | Meaning | | - | - | | `401` | Missing or invalid API key | | `402` | Balance or spend limit issue | | `400` | The provider rejected the `response_format` (for example `json_schema` on a model that only supports `json_object`, or an invalid schema) | | `422` | Invalid request body, for example `response_format` is not a JSON object | | `429` | Rate limit exceeded | # Text to Speech Source: https://developers.meshapi.ai/docs/capabilities/text-to-speech Text-to-speech, speech-to-text, voice management, and real-time streaming audio APIs. # Audio Generation Mesh API provides a full suite of audio endpoints — convert text to speech, transcribe audio files, stream TTS/STT in real time, and browse available voices — all through a single API key. All endpoints share the same base URL: `https://api.meshapi.ai/v1/audio` **Auth:** `Authorization: Bearer rsk_` on all REST requests. WebSocket endpoints accept the key via `Sec-WebSocket-Protocol: Bearer ` or `?api_key=`. *** ## Text-to-Speech `POST /v1/audio/speech` Convert a text string into audio. The brand behind a model (ElevenLabs, Sarvam, etc.) is selected automatically based on the model you pass — models like `hexgrad/kokoro-82m` and `cartesia/sonic-2` are also available. Streaming is enabled by default. ### Request body | Field | Type | Default | Description | | :- | :- | :- | :- | | `model` | string | `eleven_flash_v2_5` | Model ID. Determines which provider handles the request. | | `input` | string | — | Text to synthesize. **Required.** | | `voice` | string | — | Voice ID/name for the selected model's brand (an ElevenLabs voice ID, or a Kokoro/Cartesia voice). Browse valid IDs with `GET /v1/audio/voices` (filter by `brand`, `model`, or `search`). Required for ElevenLabs models. Gemini TTS models take a prebuilt voice name (default `Kore`). | | `speaker` | string | `ritu` | Speaker name for Sarvam models (`sarvam/bulbul:v3`). Ignored for ElevenLabs. | | `stream` | boolean | `true` | Stream audio chunks as they are generated. | | `response_format` | string | provider default | Output audio format (e.g. `mp3_44100_128`, `pcm_22050`, `wav_44100`). | | `language_code` | string | — | BCP-47 language code (e.g. `en-US`). | | `voice_settings` | object | — | Fine-tune ElevenLabs voice: `stability`, `similarity_boost`, `style`, `use_speaker_boost`, `speed`. | | `seed` | integer | — | Reproducible generation seed. | | `previous_text` | string | — | Text that came before `input` — used for better prosody. | | `next_text` | string | — | Text that comes after `input` — used for better prosody. | | `apply_text_normalization` | string | — | `auto`, `on`, or `off`. Controls ElevenLabs text normalizer. | | `enable_logging` | boolean | — | Pass `false` to opt out of ElevenLabs request logging. | | `optimize_streaming_latency` | integer | — | ElevenLabs latency-quality trade-off level (0–4). | | `pitch` | float | — | Sarvam pitch adjustment. | | `pace` | float | — | Sarvam speaking pace. | | `loudness` | float | — | Sarvam output loudness. | | `target_language_code` | string | `hi-IN` | Sarvam target language. | ### Supported output formats **Streaming (`stream: true`):** `mp3_22050_32`, `mp3_24000_48`, `mp3_44100_32/64/96/128/192`, `pcm_8000/16000/22050/24000/32000/44100/48000`, `ulaw_8000`, `alaw_8000`, `opus_48000_32/64/96/128/192` **Non-streaming (`stream: false`):** All of the above, plus `wav_8000/16000/22050/24000/32000/44100/48000` ### Response The response body is raw audio bytes with the `Content-Type` matching the requested format (e.g. `audio/mpeg`, `audio/wav`). ### Examples ```bash theme={null} curl -X POST https://api.meshapi.ai/v1/audio/speech \ -H "Authorization: Bearer rsk_..." \ -H "Content-Type: application/json" \ -d '{ "model": "elevenlabs/eleven_flash_v2_5", "input": "Hello! This is a test of the Mesh API text-to-speech endpoint.", "voice": "JBFqnCBsd6RMkjVDRZzb", "response_format": "mp3_44100_128" }' \ --output speech.mp3 ``` ```bash theme={null} curl -X POST https://api.meshapi.ai/v1/audio/speech \ -H "Authorization: Bearer rsk_..." \ -H "Content-Type: application/json" \ -d '{ "model": "elevenlabs/eleven_flash_v2_5", "input": "Hello! This is a test.", "voice": "JBFqnCBsd6RMkjVDRZzb", "stream": false, "response_format": "wav_44100" }' \ --output speech.wav ``` ```python theme={null} import httpx response = httpx.post( "https://api.meshapi.ai/v1/audio/speech", headers={"Authorization": "Bearer rsk_..."}, json={ "model": "elevenlabs/eleven_flash_v2_5", "input": "Hello! This is a test.", "voice": "JBFqnCBsd6RMkjVDRZzb", "response_format": "mp3_44100_128", }, ) with open("speech.mp3", "wb") as f: f.write(response.content) ``` ```ts theme={null} import fs from "fs"; const res = await fetch("https://api.meshapi.ai/v1/audio/speech", { method: "POST", headers: { Authorization: "Bearer rsk_...", "Content-Type": "application/json", }, body: JSON.stringify({ model: "elevenlabs/eleven_flash_v2_5", input: "Hello! This is a test.", voice: "JBFqnCBsd6RMkjVDRZzb", response_format: "mp3_44100_128", }), }); const buffer = await res.arrayBuffer(); fs.writeFileSync("speech.mp3", Buffer.from(buffer)); ``` *** ## WebSocket TTS Streaming `WS /v1/audio/speech/stream/{voice_id}` Stream text-to-speech in real time. You send text chunks as they become available (e.g. as an LLM streams tokens), and receive audio back chunk by chunk — minimising latency compared to the REST endpoint. The streaming frame protocol depends on the model family. The `voice_id` is part of the URL path. * **Standard streaming models** — e.g. `hexgrad/kokoro-82m`, `cartesia/sonic-2`, `cartesia/sonic-3`, `canopylabs/orpheus-3b-0.1-ft` — use the text-buffer protocol. * **ElevenLabs models** (`elevenlabs/*`) use ElevenLabs' native stream-input frames. ### Authentication Pass your Mesh API key in one of two ways: * `Sec-WebSocket-Protocol: Bearer rsk_...` header * `?api_key=rsk_...` query parameter ### Session limits Each WebSocket session reserves \$10 of your balance while it is open, released when it closes; a session your balance cannot cover is refused with "Insufficient balance to start a session." An account can hold 5 concurrent WebSocket sessions at once, shared with [realtime](/docs/capabilities/realtime-audio) and streaming speech-to-text sessions. ### Query parameters | Parameter | Default | Description | | :- | :- | :- | | `model_id` | `eleven_flash_v2_5` | Model to use. Determines the streaming frame protocol. | | `output_format` | `pcm_22050` | Audio format for streamed audio. See supported formats below. | | `language_code` | — | BCP-47 language code. | ElevenLabs models additionally accept `enable_logging` (`true`/`false` logging opt-out), `enable_ssml_parsing` (enable SSML in the input text), `inactivity_timeout` (seconds of inactivity before the session closes, 1–180), `sync_alignment` (return word-level alignment data with each audio chunk), `auto_mode` (optimise for low-latency, fully-automated generation), `apply_text_normalization` (`auto`, `on`, or `off`), and `seed` (reproducible seed, 0–4294967295). ### Message protocol — standard streaming models **Client → server (JSON frames)** | Frame type | Fields | Description | | :- | :- | :- | | `input_text_buffer.append` | `text: string` | Append a chunk of text to the buffer. | | `input_text_buffer.commit` | `{}` | Flush the buffer and finish synthesis. | **Server → client (JSON frames)** | Frame type | Fields | Description | | :- | :- | :- | | `conversation.item.audio_output.delta` | `delta: string` (base64) | Audio chunk. Decode from base64 to get raw audio bytes. | | `conversation.item.audio_output.done` | `{}` | Signals all audio has been sent. | #### Example ```js theme={null} const ws = new WebSocket( "wss://api.meshapi.ai/v1/audio/speech/stream/af_bella" + "?model_id=hexgrad/kokoro-82m&output_format=pcm_24000&api_key=rsk_..." ); ws.onopen = () => { // 1. Append text chunks ws.send( JSON.stringify({ type: "input_text_buffer.append", text: "Hello, this is streamed text-to-speech.", }) ); // 2. Flush + finish ws.send(JSON.stringify({ type: "input_text_buffer.commit" })); }; ws.onmessage = ({ data }) => { const frame = JSON.parse(data); if (frame.type === "conversation.item.audio_output.delta") { // Decode base64 audio and play / write to file const audioBytes = atob(frame.delta); console.log(`Received ${audioBytes.length} bytes of audio`); } if (frame.type === "conversation.item.audio_output.done") { console.log("Stream complete"); ws.close(); } }; ``` ### Message protocol — ElevenLabs models **Client → server (JSON frames)** | Frame type | Fields | Description | | :- | :- | :- | | `initializeConnection` | `text: " "`, optional `voice_settings`, `generation_config`, `pronunciation_dictionary_locators` | Must be the first message sent. | | `sendText` | `text: string`, optional `try_trigger_generation`, `voice_settings`, `flush` | Send a chunk of text to synthesize. | | `closeConnection` | `text: ""` | Signal end of input and close the session cleanly. | **Server → client (JSON frames)** | Frame type | Fields | Description | | :- | :- | :- | | `AudioOutput` | `audio: string` (base64), optional `alignment` | Audio chunk. Decode from base64 to get raw audio bytes. | | `FinalOutput` | `isFinal: true` | Signals all audio has been sent. | | `error` | `message: string` | Sent before closing on auth, format, or upstream errors. | Any credential fields (`xi-api-key`, `authorization`, `api_key`) in client frames are stripped before forwarding upstream — your upstream credentials are never exposed to the client. #### Example ```js theme={null} const ws = new WebSocket( "wss://api.meshapi.ai/v1/audio/speech/stream/JBFqnCBsd6RMkjVDRZzb" + "?model_id=elevenlabs/eleven_flash_v2_5&output_format=mp3_44100_128&api_key=rsk_..." ); ws.onopen = () => { // 1. Initialize the connection ws.send(JSON.stringify({ text: " " })); // 2. Stream text chunks ws.send(JSON.stringify({ text: "Hello, this is streamed text-to-speech." })); // 3. Close when done ws.send(JSON.stringify({ text: "" })); }; ws.onmessage = ({ data }) => { const frame = JSON.parse(data); if (frame.audio) { // Decode base64 audio and play / write to file const audioBytes = atob(frame.audio); console.log(`Received ${audioBytes.length} bytes of audio`); } if (frame.isFinal) { console.log("Stream complete"); ws.close(); } }; ``` ### Supported output formats **ElevenLabs models:** `mp3_22050_32`, `mp3_44100_32/64/96/128/192`, `pcm_16000/22050/24000/44100`, `ulaw_8000` **Standard streaming models:** `pcm`, `mp3`, `wav`, `opus`, `aac`, `flac`, each with an optional sample rate (e.g. `pcm_24000`). *** *** ## Together AI voices Together-hosted TTS models (Kokoro, Orpheus, Cartesia Sonic) don't use the ElevenLabs voice catalogue. Each model has its own set of voices, passed in the same `voice` field. `GET /v1/audio/voices` lists them alongside the ElevenLabs voices (filter with `brand` or `model`), and the references below cover the common ones. ### Kokoro — `hexgrad/kokoro-82m` Pass a voice name such as `af_bella`. Kokoro also supports **voice mixing**: join two or more voices with `+` (e.g. `af_bella+af_heart`), with optional per-voice weights in parentheses (e.g. `af_bella(2)+af_heart(1)`). A selection of the 50+ available voices (prefix key: `a`=American, `b`=British, `j`=Japanese, `z`=Chinese, `e`=Spanish, `f`=French, `h`=Hindi, `i`=Italian, `p`=Portuguese; `f`=female, `m`=male): ``` af_heart af_bella af_nova af_sarah af_sky am_adam am_echo am_michael am_onyx am_puck bf_emma bf_alice bf_lily bm_george bm_lewis bm_daniel jf_alpha jm_kumo zf_xiaoxiao zm_yunxi ef_dora ff_siwis hf_alpha hm_omega ``` ### Orpheus — `canopylabs/orpheus-3b-0.1-ft` One of: `tara`, `leah`, `jess`, `leo`, `dan`, `mia`, `zac`, `zoe`. ### Cartesia Sonic — `cartesia/sonic-2`, `cartesia/sonic-3` Pass the voice **display name** (not a voice ID). 130+ voices are available; a selection of English voices: ``` friendly sidekick nonfiction man newsman newslady calm lady helpful woman reading lady reading man british reading lady british narration lady meditation lady new york man indian lady indian man ``` ```json theme={null} { "model": "cartesia/sonic-3", "input": "Hello from Cartesia Sonic.", "voice": "friendly sidekick" } ``` **Streaming with Together models.** When `stream: true`, Together TTS models only emit raw PCM — set `response_format` to a `pcm_*` value (e.g. `pcm_16000`, `pcm_24000`). For non-streaming requests (`stream: false`) you can also use `mp3_*` or `wav_*`. ## Available voices `GET /v1/audio/voices` returns one catalogue of TTS voices across model brands: ElevenLabs, Cartesia, Kokoro, Orpheus and others. ```bash theme={null} curl "https://api.meshapi.ai/v1/audio/voices?brand=hexgrad" \ -H "Authorization: Bearer rsk_YOUR_KEY" ``` All filters are optional and case-insensitive: | Parameter | Description | | - | - | | `brand` | Model brand, e.g. `elevenlabs`, `hexgrad`, `canopylabs`, `cartesia` | | `model` | A model ID, e.g. `hexgrad/kokoro-82m`. ElevenLabs voices are not tied to a model, so a `model` filter alone drops them; use `brand=elevenlabs` | | `search` | Substring match on voice ID or name | **Response:** ```json theme={null} { "voices": [ { "voice_id": "af_bella", "name": "Bella", "brand": "hexgrad", "model": "hexgrad/kokoro-82m", "language": "en-us", "gender": "female", "preview_url": null } ], "count": 1, "truncated": false } ``` The whole list comes back in one response, with no pagination. ElevenLabs voices carry `model: null` (they work with every ElevenLabs model). `truncated: true` means a brand's list was cut short, so the catalogue is partial. A brand that fails to load is skipped rather than failing the request. ## Available models | Model | Provider | Notes | | - | - | - | | `gpt-4o-mini-tts` | OpenAI | OpenAI TTS | | `elevenlabs/eleven_flash_v2_5` | ElevenLabs | Low latency | | `elevenlabs/eleven_multilingual_v2` | ElevenLabs | Multilingual | | `elevenlabs/eleven_v4` / `elevenlabs/eleven_v4_turbo` | ElevenLabs | Eleven v4 | | `google/gemini-2.5-flash-tts` | Google | Gemini TTS. `response_format`: `pcm_24000`, or `wav_24000` with `stream: false` | | `google/gemini-3.1-flash-tts` | Google | Gemini TTS | | `google/gemini-3.8-flash-tts` / `google/gemini-3.8-flash-lite-tts` | Google | Gemini TTS | | `sarvam/bulbul:v3` | Sarvam | Indian languages (Hindi, Tamil, Telugu, etc.) | | `hexgrad/kokoro-82m` | Together | Open-weight, 50+ voices, supports voice mixing | | `canopylabs/orpheus-3b-0.1-ft` | Together | Expressive English voices | | `cartesia/sonic-2` | Together | Cartesia Sonic 2 — 130+ voices | | `cartesia/sonic-3` | Together | Cartesia Sonic 3 (latest) — 130+ voices | Together TTS voices are passed in the `voice` field but follow each model's own naming. See [Together AI voices](#together-ai-voices) above. Check `GET /v1/models` for the full live list of TTS models. # Tool Calling Source: https://developers.meshapi.ai/docs/capabilities/tool-calling Let models call your functions — the request/response cycle, multi-turn flow, and the provider-specific details MeshAPI handles for you. Tool calling lets a model ask your application to run a function and then continue with the result. MeshAPI implements the OpenAI-compatible `tools` / `tool_choice` interface on `/v1/chat/completions`, so existing OpenAI tool-calling code works unchanged. *** ## The cycle 1. You send a request with a `tools` array describing the functions available. 2. The model replies with `finish_reason: "tool_calls"` and one or more `tool_calls`. 3. You execute the function yourself and append the result as a `role: "tool"` message. 4. You send the conversation back; the model produces its final answer. ### 1. Declare the tools ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o-mini", "messages": [{ "role": "user", "content": "What is the weather in Chennai?" }], "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "Get the current weather for a city.", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "City name" } }, "required": ["city"] } } } ] }' ``` ### 2. The model asks for a call ```json theme={null} { "choices": [{ "finish_reason": "tool_calls", "message": { "role": "assistant", "tool_calls": [{ "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"Chennai\"}" } }] } }] } ``` `arguments` is a **JSON-encoded string**, not an object — parse it before use. ### 3. Return the result Append the assistant message *and* a `tool` message carrying the matching `tool_call_id`: ```json theme={null} { "model": "openai/gpt-4o-mini", "messages": [ { "role": "user", "content": "What is the weather in Chennai?" }, { "role": "assistant", "tool_calls": [{ "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"Chennai\"}" } }] }, { "role": "tool", "tool_call_id": "call_abc123", "content": "{\"temp_c\": 31, \"condition\": \"humid\"}" } ], "tools": [ ... ] } ``` Keep sending `tools` on the follow-up. Every `tool_call_id` must be answered by exactly one `tool` message. *** ## Controlling when tools are used | `tool_choice` | Behaviour | | - | - | | `"auto"` (default) | Model decides | | `"none"` | Never call a tool | | `"required"` | Must call at least one tool | | `{"type": "function", "function": {"name": "..."}}` | Force a specific function | *** ## Tool calling disables the response cache Any request carrying a `tools` array is **never served from, or written to, the [gateway response cache](/docs/capabilities/caching)** — a tool call depends on live state, so replaying a stored one would be wrong. This is silent. If you are relying on caching for cost and you add tools to a request, that saving disappears with no error and no header. It is the correct behaviour, but worth knowing before it shows up as a bill. *** ## Gemini thinking models Gemini's thinking models attach a **thought signature** to each tool call, and Vertex rejects a follow-up whose tool history is missing it. The OpenAI wire format has no slot for this, so MeshAPI exposes it in two places: * on the tool call itself, as `tool_calls[].thought_signature` * in a side channel, `message.reasoning_details`, for clients that strip unknown fields from `tool_calls` **Echo it back verbatim** in the assistant message when you continue the conversation. Some frameworks — LangChain among them — strip both fields automatically. MeshAPI detects the resulting rejection and retries once with the unsigned tool exchanges demoted to plain text, so your request still succeeds. You lose the thinking context on that turn, but you do not get an error. If you can preserve the signature, do; if your framework won't, the fallback covers you. *** ## Related * [Caching](/docs/capabilities/caching) — why tools bypass the response cache * [Structured Output](/docs/capabilities/structured-output) — for shaping a response rather than calling code * The **SDKs** tab has per-language tool-calling examples for Python, Node.js, and Go # Video Generation Source: https://developers.meshapi.ai/docs/capabilities/video-generation Turn a prompt, a still image, a clip, or an audio track into video with one async API call. Give Mesh a prompt and you get back a video. Give it more to work with — a still to open on, a second still to end on, reference images, a clip, a soundtrack — and you get back a video that matches what you supplied. Generating a video takes minutes rather than seconds, so this endpoint is asynchronous. Your `POST` returns a task ID straight away, and the finished video arrives either when you [ask for it](#poll-for-the-result) or through a [webhook](#have-the-result-delivered). Everything goes to `https://api.meshapi.ai` and carries your API key as `Authorization: Bearer rsk_`. See [Authentication](/docs/getting-started/authentication) if you need one. ## Your first video A prompt in, a task ID out. ```bash theme={null} curl -X POST https://api.meshapi.ai/v1/video/generations \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "byteplus/dreamina-seedance-2-0", "content": [ { "type": "text", "text": "A cinematic drone shot over a misty mountain valley at dawn" } ], "resolution": "1080p", "ratio": "16:9", "duration": 5 }' ``` ```python theme={null} import requests res = requests.post( "https://api.meshapi.ai/v1/video/generations", headers={"Authorization": "Bearer rsk_YOUR_KEY"}, json={ "model": "byteplus/dreamina-seedance-2-0", "content": [ {"type": "text", "text": "A cinematic drone shot over a misty mountain valley at dawn"} ], "resolution": "1080p", "ratio": "16:9", "duration": 5, }, ) task = res.json() print(task["id"], task["status"]) ``` ```javascript theme={null} const res = await fetch("https://api.meshapi.ai/v1/video/generations", { method: "POST", headers: { Authorization: "Bearer rsk_YOUR_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ model: "byteplus/dreamina-seedance-2-0", content: [ { type: "text", text: "A cinematic drone shot over a misty mountain valley at dawn" }, ], resolution: "1080p", ratio: "16:9", duration: 5, }), }); const task = await res.json(); console.log(task.id, task.status); ``` The response is short, because there is no video yet: ```json theme={null} { "id": "t-xxxxxxxxxxxxxxxx", "status": "queued", "unsupported_params": null } ``` Hold on to that `id` — [Getting the result](#getting-the-result) turns it into a video. Log [`unsupported_params`](#what-the-route-dropped) beside it while you are there: this is the only response that ever carries it, and it tells you whether the request that ran was the request you sent. Three endpoints cover everything on this page: | Method | Path | What it does | | :- | :- | :- | | `POST` | `/v1/video/generations` | Start a generation | | `GET` | `/v1/video/generations/{id}` | Check one task and collect its video | | `GET` | `/v1/video/generations` | List your recent tasks | The examples further down show the **request body** only. Send each of them exactly like the curl above — same URL, same two headers. ## Giving the model something to work from The `content` array carries your prompt and every piece of media that goes with it. Each item says two separate things: * **`type`** is the *modality* — `text`, `image_url`, `video_url`, `audio_url` or `document_url`. * **`role`** is what that media is *for* — `first_frame`, `last_frame`, `reference_image`, `reference_video`, `reference_audio` or `reference_document` (plus the [finer roles](#finer-roles-documents-and-keyframes) below). Keeping them apart is what lets one request carry two images that mean different things. Two `image_url` items are ambiguous on their own; a `first_frame` and a `last_frame` are not. | `type` | Where the payload goes | | :- | :- | | `text` | `text` — your prompt | | `image_url` | `image_url.url` | | `video_url` | `video_url.url` | | `audio_url` | `audio_url.url` | | `document_url` | `document_url.url`: a public URL the model reads for reference | | `draft_task` | `draft_task.id` — see [Preview first, then render](#preview-first-then-render) | Any of the URLs may be a publicly reachable link or a Base64 data URI (`data:image/jpeg;base64,...` — keep the MIME prefix). A public URL has to be fetchable without authentication, since the model fetches it itself. `role` is optional. Leave it out and each item falls back to its most common meaning: | Item with no `role` | Read as | | :- | :- | | `image_url` | `first_frame` | | `video_url` | `reference_video` | | `audio_url` | `reference_audio` | | `document_url` | `reference_document` | So the simplest image-to-video request needs no `role` at all. You need one as soon as a request pins a closing frame, or uses images as references rather than as frames. A `role` that contradicts its item — `first_frame` on an audio item — is rejected with `422`. So is a role the model you picked does not offer: rather than drop the item and generate something you did not ask for, the submit refuses it and names the offending entry — by its position in `content` on most models, by role alone on the rest. Neither costs you a generation, but both cost you a round trip — [check first](#finding-out-what-a-model-accepts). ### Pick one input scenario, not two Roles are not freely combinable. A request is one of these shapes, and on most models they are **mutually exclusive**: | Scenario | What you send | | :- | :- | | **Text to video** | A prompt, no media | | **First frame** | One image, optional prompt | | **First and last frame** | Two images, optional prompt | | **References** | Reference images, a clip and/or audio, optional prompt | Mixing two of them is the most common way a video request fails. A `first_frame` alongside a `reference_image` is not a richer request — it is an invalid one, and you often find out **late**: the submit returns `200` with a task ID and the task goes `failed` minutes later. To pin both ends *and* supply references, use first-and-last-frame — it is the only shape that guarantees the output matches the images you gave. References can be nudged toward a particular opening with wording in the prompt, but nothing guarantees the match. Text to video is not universal either: a model published specifically for image-to-video or for upscaling needs its input media and turns down a prompt-only request with a `400`. ### Start from an image One image and a prompt: the video opens on your still and moves from there. `role` is optional here, but writing it makes the intent obvious. ```json theme={null} { "model": "byteplus/dreamina-seedance-2-0", "content": [ { "type": "image_url", "image_url": { "url": "https://example.com/first-frame.jpg" }, "role": "first_frame" }, { "type": "text", "text": "The subject slowly turns and walks toward the camera" } ], "duration": 5, "ratio": "9:16", "seed": 42 } ``` ### Open and close on exact frames Give the model both ends and it fills in the middle. Here `role` is required — two images with no roles are ambiguous. ```json theme={null} { "model": "byteplus/dreamina-seedance-2-0", "content": [ { "type": "image_url", "image_url": { "url": "https://example.com/open.jpg" }, "role": "first_frame" }, { "type": "image_url", "image_url": { "url": "https://example.com/close.jpg" }, "role": "last_frame" }, { "type": "text", "text": "The camera pushes in slowly as the light shifts from dawn to midday" } ], "duration": 5, "ratio": "adaptive" } ``` The two images may be identical, and they need not share an aspect ratio — the first frame wins and the last frame is cropped to match. A closing frame is a rarer ability than an opening one, and models that offer it usually want a `first_frame` with it. ### Work from reference images References say *what should appear*, not *where in time it appears* — a face, a product, a look. Use them when you want the same thing to show up in the video without dictating any particular frame. ```json theme={null} { "model": "byteplus/dreamina-seedance-2-5", "content": [ { "type": "text", "text": "A product reveal with dramatic lighting, matching the reference style" }, { "type": "image_url", "image_url": { "url": "https://example.com/product.jpg" }, "role": "reference_image" }, { "type": "image_url", "image_url": { "url": "https://example.com/lighting-mood.jpg" }, "role": "reference_image" } ], "duration": 8, "ratio": "adaptive" } ``` How many reference images you may send ranges from one to thirty depending on the model. `supports_video_reference_image` tells you whether the role is accepted at all; the model governs how many. ### Name your subjects A flat list of five reference images is five unrelated references — nothing in it says that the first two are the same person and the third is the bottle she picks up. `subject` says so. Pick a name, put it on every item that shows that subject, and refer to it in the prompt as `@name`. ```json theme={null} { "model": "kling/kling-3.0-omni", "content": [ { "type": "text", "text": "@hero picks up @bottle and turns toward the window" }, { "type": "image_url", "image_url": { "url": "https://example.com/hero-front.jpg" }, "role": "reference_image", "subject": "hero" }, { "type": "image_url", "image_url": { "url": "https://example.com/hero-profile.jpg" }, "role": "reference_image", "subject": "hero" }, { "type": "image_url", "image_url": { "url": "https://example.com/bottle.jpg" }, "role": "reference_image", "subject": "bottle" } ], "duration": 5, "ratio": "adaptive" } ``` Four things to know: **Order matters inside a group: the first item is the main view, the rest are further angles of the same subject.** Nothing about the request looks ordered, which is what makes this easy to get wrong — put the clearest, most head-on image first. **Names are strict and match exactly.** 1–64 characters of letters, digits, underscore or hyphen. Spaces, `@`, dots and non-ASCII are rejected with a `422`, as is an empty name, and matching is case-sensitive — `Hero` and `hero` are two different subjects. In the prompt, only names you actually declared are treated as citations; every other `@` in your text is left exactly as you typed it. **`subject` belongs on reference items only.** On a `first_frame`, on a `text` item, or anywhere else it returns `422`. **Grouping never costs you anything.** A model with no notion of subjects still receives every image as an ordinary reference, so the request above works wherever reference images do — it simply loses the distinction between the two subjects. You never need two versions of a request. Not every model reads names. Some match reference images by the order you sent them instead, and there an `@name` is just text in your prompt — the images still reach the model, you only lose which-is-which. Describing your subjects in prose as well ("the man in the blue coat picks up the green bottle") costs nothing and reads correctly either way. ### Keeping a character consistent Models that can hold one character across a whole video generally screen the reference first, for likeness and for copyright, and decline anything that looks like a real person. That screening is the usual reason a reference which "should" work appears to have been ignored. Where a model works that way, Mesh registers your reference before generating so the same face survives from shot to shot. There is nothing to switch on — send the image as a public URL or a data URI exactly as you would otherwise. **The first request with a brand-new reference can come back `422`, saying the reference is still being prepared.** Registration runs in the background with no fixed completion time. Retry the same request; the retry joins the registration already in flight rather than starting a second one, and after that the image is immediate. **References are matched on content, not on URL.** The same image behind a re-signed link or a CDN variant is one reference, so changing the URL of an image you have already used costs nothing. **Send the image, not an ID.** `asset://` identifiers from elsewhere are rejected with `422`. Supply a URL or a data URI and Mesh does the registering. By supplying a reference you confirm you hold the rights to it and that it is not the likeness of a real person. Registered references are kept while you keep using them and removed once you stop. See the [Terms of Service](https://meshapi.ai/terms). ### Work from a clip A `reference_video` is a clip for the model to imitate, edit or extend — motion, pace, framing, or the footage itself. ```json theme={null} { "model": "byteplus/dreamina-seedance-2-5", "content": [ { "type": "text", "text": "Keep the camera move but restage it at night in the rain" }, { "type": "video_url", "video_url": { "url": "https://example.com/clip.mp4" }, "role": "reference_video" } ], "ratio": "adaptive" } ``` Fewer models take a clip than take an image, so check `supports_video_reference_video` first. To raise the resolution of an existing video rather than reimagine it, see [Upscale an existing video](#upscale-an-existing-video). ### Work from a soundtrack A `reference_audio` item gives the model audio to follow — a beat to cut to, a voice to match a mouth to. ```json theme={null} { "model": "byteplus/dreamina-seedance-2-5", "content": [ { "type": "text", "text": "A dancer in a bare studio, moving in time with the music" }, { "type": "audio_url", "audio_url": { "url": "https://example.com/soundtrack.mp3" }, "role": "reference_audio" } ], "duration": 8, "ratio": "adaptive" } ``` Audio is the least widely supported input of the three — `supports_video_reference_audio` is the flag. Some models also will not take audio as the *only* media in a request; if one turns this down, send a reference image or a clip alongside it. ### Upscale an existing video Upscalers take a video and return a higher-resolution one. The request is a different shape: the source clip is the **only** input, and a prompt, `ratio`, `duration` or `frames` are not accepted. ```bash theme={null} curl -X POST https://api.meshapi.ai/v1/video/generations \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "bytedance/bytedance-video-upscaler", "content": [ { "type": "video_url", "video_url": { "url": "https://example.com/clip.mp4" } } ] }' ``` Collect the result exactly as with any other task. A `video_url` item is required — a prompt on its own returns `400`, because there is nothing to upscale. The target resolution is fixed by the model, so `resolution` does not change the output, but it does pick the per-second price tier you are billed at (`720p` when omitted). ### Finer roles, documents and keyframes Three roles narrow an ordinary reference role to a slot a few models define. A model without the finer slot still receives the media as the role it narrows, so naming one never costs you a route: | Role | Narrows | For | | :- | :- | :- | | `scene_image` | `reference_image` | A picture of the scene or setting | | `source_video` | `reference_video` | The clip an edit works on. Required for `match_source_duration` | | `background_audio` | `reference_audio` | A music bed | `reference_document` (on a `document_url` item) narrows nothing: a model that cannot read a document refuses the request rather than generating without it. A `reference_image` can be pinned to one moment of the video with `frame_index` (a frame number, counting from 0) or `timestamp` (seconds from the start), one image per moment. The units are not interchangeable: each reaches only the models that define it, and a model with no keyframe concept still receives the image as an ordinary reference. ### What you can send The ceilings below are the widest any current model allows. A given model may be tighter, so treat a refusal that names a size, a duration or a count as that model's limit rather than the platform's. **Images** — `jpeg`, `png`, `webp`, `bmp`, `tiff`, `gif`, and `heic`/`heif` on some models. Each side 300–6000 px, aspect ratio between 0.4 and 2.5, up to 30 MB each. One image for a first frame, two for first-and-last, up to 30 references. **Clips** — `mp4` or `mov`, H.264/AVC or H.265/HEVC video with AAC or MP3 audio, 24–60 fps. Each side 300–6000 px, aspect ratio 0.4–2.5, up to 200 MB and 2–30 s each, up to 10 clips totalling 30 s. An edit job needs at least 4 s to work from. **Audio** — `wav` or `mp3`, up to 15 MB and 2–30 s each, up to 10 clips totalling 30 s. **The whole request body has a hard 32 MiB ceiling** (33,554,432 bytes) on every Mesh endpoint, enforced at the edge — an oversized request never reaches the API and comes back as a plain HTML `413 Request Entity Too Large` page rather than a Mesh error. See [Rate Limits & Spend Caps](/docs/getting-started/rate-limits#request-body-size). So do not Base64-encode large files: encoding inflates a payload by about a third, and a \~24 MB file already breaks the body limit on its own. Public URLs are the only way to reach the per-file caps above. ### Older spellings Before `role` existed, the purpose went in `type` — `"type": "first_frame"` and friends. Those requests still work and still mean what they meant, so nothing you already send needs changing. Write new ones with the modality in `type` and the purpose in `role`: the older form is read as the role it always meant, so an item the model cannot take is refused with a `422` either way. The modern spelling is the one that works everywhere. ## Shaping the output Everything on this page other than `model` and `content` is an optional top-level field. Mesh sets no default for any of them — omitted means absent, not "the model's default". **A field name this endpoint does not recognise is dropped without a word.** There is no `422` and no warning: the request returns `200`, the task runs, and your parameter was never part of it. A typo like `negitive_prompt`, or `aspect_ratio` where the field is `ratio`, costs you a whole generation before you notice. Check your spelling against the tables below. Sending a *real* field to a model that does not implement it is a different case, and that one you do hear about — see [What the route dropped](#what-the-route-dropped). ### The basics | Field | Type | Description | | :- | :- | :- | | `resolution` | string | Output resolution, e.g. `720p`, `1080p`, or an exact pixel size, `1280x720` or `1280*720`, on the models that take one (the **Size** column [below](#what-each-model-accepts)). | | `ratio` | string | Aspect ratio, e.g. `16:9`, `9:16`, `1:1`. `adaptive` follows your input media. | | `duration` | integer | Target length in seconds, 1–300. | | `match_source_duration` | boolean | Make the video as long as the `source_video` clip you sent. Needs exactly one `source_video` item; cannot be combined with `duration`. | | `frames` | integer | A frame count instead of a duration, for sub-second lengths. Only some models take it. Send it or `duration`, not both. | | `generate_audio` | boolean | Generate an audio track alongside the video. Priced differently when on. | | `return_last_frame` | boolean | Also return a still of the final frame, handy as the opening frame of the next clip. | | `seed` | integer | Fix the seed to make a generation reproducible. | | `camera_fixed` | boolean | Lock the camera — no camera movement. | | `watermark` | boolean | Add a watermark to the output. | | `draft` | boolean | Render a fast preview instead of the finished video. See [Preview first, then render](#preview-first-then-render). | | `callback_url` | string | Where to POST the finished task. BytePlus, Kling and MiniMax models only; any other model returns `422`. See [Have the result delivered](#have-the-result-delivered). | | `service_tier` | string | Ask for a particular service tier, where the model offers a choice. | | `execution_expires_after` | integer | Seconds before an unfinished task gives up. BytePlus models only. Default `172800` (48 hours). | | `priority` | integer 0–9 | How urgent this job is. Higher is more urgent. | | `safety_identifier` | string | A stable, anonymised id for the end user the request was made for, used for abuse detection. | | `previous_task_id` | string | Edit one of your own earlier `succeeded` videos instead of uploading it again. Describe the change in a `text` item. Token-priced Gemini Omni Flash models only (`google/gemini-omni-flash-preview`); anything else returns `422`. | | `external_task_id` | string | Your own id for the task, forwarded to Kling so the task is findable by your identifier. Ignored elsewhere. | | `fast_pretreatment` | boolean | MiniMax Hailuo: skip some prompt pre-processing so generation starts sooner. | | `tags` | object | Your own labels, as an object of up to 16 string pairs. Filter and group spend by them in usage reports. | ### Finer control These shape *how* the model generates rather than what comes out of the pipe. Support varies by model, which is the whole reason for the [capability flags](#finding-out-what-a-model-accepts). | Field | Type | Values | What it does | | :- | :- | :- | :- | | `negative_prompt` | string | — | What to keep out of the video. The length cap varies; 2500 characters is a safe assumption. | | `guidance_scale` | number | `1.0`–`10.0` | How literally to follow your prompt versus inventing freely. Higher is more literal. Outside the range is a `422`. | | `prompt_extend` | boolean | — | Let the model flesh out your prompt with detail before generating. Helps short prompts, costs time. | | `style` | string | `general`, `anime` | Visual style of the output. | | `movement_amplitude` | string | `auto`, `small`, `medium`, `large` | How much the subjects in frame are allowed to move. | | `multi_shot` | boolean | — | Let the model cut between camera angles instead of holding one shot. | | `thinking_level` | string | `default`, `low`, `high` | How hard the model thinks about your prompt before generating. Higher is slower. | | `num_inference_steps` | integer | `1`–`100` | Denoising steps. More steps, more detail, longer wait. Outside the range is a `422`. | | `web_search` | boolean | — | Let the model look things up while generating, for prompts about something current. Adds latency. | | `bgm` | boolean | — | Add background music to the generated audio. | | `keep_original_sound` | boolean | — | Keep the audio from your reference clip instead of generating new audio. | | `audio_type` | string | `all`, `speech_only`, `sound_effect_only` | Which parts of the audio track to generate. Only meaningful with `generate_audio: true`. | | `output_format` | string | e.g. `mp4`, `mov` | Output container. `mp4` plays everywhere; `mov` carries higher colour precision for grading, compositing, or as the input to a later edit. The values that mean anything are the model's. | | `bitrate_mode` | string | `standard`, `high` | `high` encodes at a higher bitrate — crisper picture, bigger file. | | `enable_safety_checker` | boolean | — | Run a safety check on the output. Turning it off can return unsafe content. | | `omni_reference_task_type` | string | `auto`, `reference`, `edit`, `extend` | Which reference job you meant. See [Say which reference job you mean](#say-which-reference-job-you-mean). | `guidance_scale` and `num_inference_steps` are the only two values Mesh range-checks. Every other field above is forwarded exactly as you wrote it, so the values listed are the ones that mean something to the models implementing that field, not a set Mesh polices. Send `output_format: "webm"` and you will not get a `422` from us — you get whatever the model makes of an unfamiliar container, which may be an error and may be nothing at all. `negative_prompt` is the least predictable of these. Support varies model by model rather than in tidy families — two builds of the same family often differ — which is why it has a capability flag of its own. If a video plainly ignored your negative prompt, read `supports_video_negative_prompt` before you start rewording it. ### Say which reference job you mean Some models can do several different jobs from reference material: generate something new, edit an existing clip, or extend one. Each job has its own constraints, and if you let the model guess which one you meant, a request whose parameters contradict its guess is accepted and fails minutes later. `omni_reference_task_type` names the job so the constraints are checked at submit instead: | Value | The job | Checked when you submit | | :- | :- | :- | | `auto` (default) | The model infers it from your media and prompt | Nothing — a mismatch fails later | | `reference` | Generate a new video from references | No extra constraints | | `edit` | Change the picture or audio of an existing clip | Needs a `reference_video`; `ratio` must be `adaptive` or omitted; `duration` must be omitted | | `extend` | Continue an existing clip | Needs a `reference_video`; `ratio` must be `adaptive` or omitted | `omni_reference_task_type` is a Seedance 2.5 parameter (e.g. `byteplus/dreamina-seedance-2-5`); other Seedance models return `422`. The model's own limits, such as an `edit` source clip of 4–30 s, are still checked by the model after you submit. Naming the job is worth it even when you are confident: the failure you avoid is the expensive kind to debug. ```json theme={null} { "model": "byteplus/dreamina-seedance-2-5", "content": [ { "type": "text", "text": "Continue the shot as the car disappears around the corner" }, { "type": "video_url", "video_url": { "url": "https://example.com/clip.mp4" }, "role": "reference_video" } ], "omni_reference_task_type": "extend", "ratio": "adaptive" } ``` The model still re-reads your prompt when it runs. If it decides you asked for a different job than the one you named, the task fails anyway — so keep the prompt consistent with the job. ### Preview first, then render `draft: true` renders a fast, rough version of the same request. When you like one, send its task ID back as a `draft_task` item and the model renders it properly instead of generating something new from scratch. ```json theme={null} { "model": "byteplus/dreamina-seedance-2-0", "content": [ { "type": "draft_task", "draft_task": { "id": "t-xxxxxxxxxxxxxxxx" } } ] } ``` ## Finding out what a model accepts Video models differ far more than text models do — one takes a closing frame, the next has no concept of one. Two fields answer the question: one before you send, one after. ### Check the flags before you send `GET /v1/models` returns a capability flag per input, at the top level of each model entry beside `supports_tools` and the rest. Read them at runtime rather than keeping a list of your own: support genuinely changes between two builds of the same family. ```bash theme={null} curl "https://api.meshapi.ai/v1/models?type=video" \ -H "Authorization: Bearer rsk_YOUR_KEY" \ | jq '.[] | select(.supports_video_last_frame) | .id' ``` Start with `supports_video_generation`, which says the model makes video at all. These six then describe what it takes **in**: | Flag | `true` when the model accepts | | :- | :- | | `supports_video_first_frame` | `role: "first_frame"` — an opening image | | `supports_video_last_frame` | `role: "last_frame"` — a pinned closing frame | | `supports_video_reference_image` | `role: "reference_image"` | | `supports_video_reference_video` | `role: "reference_video"` | | `supports_video_reference_audio` | `role: "reference_audio"` | | `supports_video_negative_prompt` | the `negative_prompt` parameter | Five of the six are named for a content role. The last one is not — it is named for the top-level `negative_prompt` parameter, and there is no `negative_prompt` role to put on a `content` item. **All six read `false` on every model that is not a video model.** An all-`false` set almost never means "a video model that accepts no input" — it means you are not looking at a video model. Check `supports_video_generation` first; the other six only mean something once it is `true`. A model can be served by more than one route, and the flags can differ between them. `GET /v1/models` reports the route you are most likely to get; `GET /v1/models/{model_id}/providers` returns the same six for every route that can serve the model. So the model entry answers "will the route I am likely to get take a last frame", and the providers endpoint answers "does any route take one". ```bash theme={null} curl https://api.meshapi.ai/v1/models/byteplus/dreamina-seedance-2-5/providers \ -H "Authorization: Bearer rsk_YOUR_KEY" ``` The full field list is on [Models](/docs/reference/models#model-capabilities). ### What each model accepts The flags say WHICH inputs a model takes. This says which VALUES its parameters accept — the resolutions it renders, how long a clip may be, the aspect ratios it offers, and how many media items you may attach. Read it before sizing a request: a value a model does not render is a `422` at submit, and a parameter it does not implement is dropped and named in [`unsupported_params`](#what-the-route-dropped). | Model | Resolution | Size | Duration | Aspect ratio | Max media inputs | | :- | :- | :- | :- | :- | :- | | `alibaba/happyhorse-1.0` | `720p`, `1080p` | — | — | `16:9`, `1:1`, `3:4`, `4:3`, `9:16` | — | | `alibaba/happyhorse-1.1` | `480p`, `720p`, `1080p` | — | — | `16:9`, `1:1`, `21:9`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21` | — | | `alibaba/wan-2.5` | `480p`, `720p`, `1080p` | `1080*1920`, `1088*832`, `1248*1632`, `1280*720`, `1440*1440`, `1632*1248`, `1920*1080`, `480*832`, `624*624`, `720*1280`, `832*1088`, `832*480`, `960*960` | 5–10s | — | — | | `alibaba/wan-2.6` | `720p`, `1080p` | `1080*1920`, `1088*832`, `1248*1632`, `1280*720`, `1440*1440`, `1632*1248`, `1920*1080`, `720*1280`, `832*1088`, `960*960` | 5s, 10s, 15s | — | — | | `alibaba/wan-2.7` | `720P`, `1080P` | — | — | `16:9`, `1:1`, `3:4`, `4:3`, `9:16` | — | | `alibaba/wan-3.0` | `720p`, `1080p` | — | 2–30s | — | frames ×2, ref audio ×5, ref images ×10, ref videos ×5 | | `alibaba/wan-3.0-prime` | `720p`, `1080p` | — | 2–30s | — | frames ×2, ref audio ×5, ref images ×10, ref videos ×5 | | `atlascloud/van-2.5` | `720p`, `1080p` | `1080*1920`, `1280*720`, `1920*1080`, `720*1280` | 5s, 10s | — | — | | `atlascloud/van-2.6` | `1080p` | `1080*1920`, `1248*1632`, `1440*1440`, `1632*1248`, `1920*1080` | 5s, 10s, 15s | — | — | | `atlascloud/wan-2.2-turbo` | `480p`, `720p`, `1080p` | — | 5s | — | — | | `bytedance/seedance-1.0-lite`
ignores resolution, ratio | — | — | 1–10s | — | — | | `bytedance/seedance-1.0-pro` | — | — | 2–12s | — | — | | `bytedance/seedance-2.0` | `480p`, `720p`, `720p-SR`, `1080p`, `1080p-SR`, `1440p-SR`, `4k` | — | 4–15s · `auto` | `16:9`, `1:1`, `21:9`, `3:4`, `4:3`, `9:16`, `adaptive` | — | | `bytedance/seedance-2.0-fast` | `480p`, `720p`, `720p-SR`, `1080p-SR`, `1440p-SR` | — | 4–15s · `auto` | `16:9`, `1:1`, `21:9`, `3:4`, `4:3`, `9:16`, `adaptive` | — | | `bytedance/seedance-2.0-mini` | `480p`, `720p`, `720p-SR`, `1080p-SR`, `1440p-SR` | — | 4–15s · `auto` | `16:9`, `1:1`, `21:9`, `3:4`, `4:3`, `9:16`, `adaptive` | — | | `byteplus/dreamina-seedance-2-0` | — | — | 4–15s | — | — | | `byteplus/dreamina-seedance-2-0-fast` | — | — | 4–15s | — | — | | `byteplus/dreamina-seedance-2-5` | — | — | 4–30s | — | — | | `byteplus/seedance-1-0-pro-fast` | — | — | 2–12s | — | — | | `byteplus/seedance-1-5-pro` | — | — | 4–12s | — | — | | `character-ai/ovi`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `fastvideo/fastwan-qad-fp8-1.3b` | `480p` | — | 5s | — | — | | `google/gemini-omni-flash` | `720p`, `1080p`, `4k` | — | 4s, 6s, 8s, 10s | `16:9`, `9:16` | — | | `google/gemini-omni-flash-preview`
values are provider-wide, not specific to this model | `360p`, `720p`, `1080p`, `4k` | — | — | `16:9`, `9:16` | — | | `google/gemini-omni-flash-video`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `google/veo-3.0`
ignores resolution, ratio | — | — | 1–10s | — | — | | `google/veo-3.0-fast`
ignores resolution, ratio | — | — | 1–10s | — | — | | `google/veo-3.1` | `720p`, `1080p` | — | — | `16:9`, `9:16` | — | | `google/veo-3.1-fast` | `720p`, `1080p` | — | — | `16:9`, `9:16` | — | | `google/veo3.1-lite` | `720p`, `1080p` | — | 4s, 6s, 8s | `16:9`, `9:16` | — | | `heygen/heygen-avatar-iv` | `720p`, `1080p` | — | — | — | — | | `heygen/heygen-avatar-v` | `720p`, `1080p` | — | — | — | — | | `heygen/heygen-video-agent` | — | — | 5–300s | — | — | | `kling/kling-1.0`
ignores resolution | — | — | 5s, 10s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-1.5`
ignores resolution | — | — | 5s, 10s | — | — | | `kling/kling-1.6`
ignores resolution | — | — | 5s, 10s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-2.0-master`
ignores resolution | — | — | 5s, 10s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-2.1-master`
ignores resolution | — | — | 5s, 10s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-2.1-pro`
ignores resolution, ratio | — | — | 1–10s | — | — | | `kling/kling-2.5-turbo`
ignores resolution | — | — | 5s, 10s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-2.5-turbo-pro` | — | — | 5s, 10s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-2.6`
ignores resolution | — | — | 5s, 10s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-2.6-pro` | — | — | 5s, 10s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-3.0`
ignores resolution | — | — | 3–15s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-3.0-omni` | `720p`, `1080p`, `4k` | — | 3–15s | — | — | | `kling/kling-3.0-turbo` | `720p`, `1080p` | — | 3–15s | — | — | | `kling/kling-o3-4k`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `kling/kling-o3-pro`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `kling/kling-o3-standard`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `kling/kling-v3-standard`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `kling/kling-v3-turbo-pro`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `kling/kling-v3-turbo-standard`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `kling/kling-v3.0-4k` | — | — | 3–15s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-v3.0-pro` | `1080P`, `1440P-SR` | — | 3–15s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-v3.0-std` | `720P`, `1080P-SR`, `1440P-SR` | — | 3–15s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-video-o1` | `720p`, `1080p` | — | 5s, 10s | — | — | | `kling/kling-video-o3-4k` | — | — | 3–15s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-video-o3-pro` | — | — | 3–15s | `16:9`, `1:1`, `9:16` | — | | `kling/kling-video-o3-std` | — | — | 3–15s | `16:9`, `1:1`, `9:16` | — | | `klingai/kling-video-2-6-pro` | — | — | 5s, 10s | — | frames ×2, ref images ×1, ref videos ×1 | | `klingai/kling-video-2-6-standard` | — | — | — | — | ref images ×1, ref videos ×1 | | `klingai/kling-video-3-0-4k` | — | — | 3–15s | — | frames ×2 | | `klingai/kling-video-3-0-omni-4k` | — | — | 3–15s | — | frames ×2, ref videos ×1 | | `klingai/kling-video-3-0-omni-pro` | `1080p` | — | 3–15s | — | frames ×2, ref images ×7, ref videos ×1 | | `klingai/kling-video-3-0-omni-standard` | `720p` | — | 3–15s | — | frames ×2, ref images ×7, ref videos ×1 | | `klingai/kling-video-3-0-pro` | — | — | 3–15s | — | frames ×2, ref images ×1, ref videos ×1 | | `klingai/kling-video-3-0-standard` | — | — | 3–15s | — | frames ×2, ref images ×1, ref videos ×1 | | `klingai/kling-video-3-0-turbo` | `720p`, `1080p` | — | 3–15s | — | frames ×1 | | `klingai/kling-video-o1-pro` | — | — | — | — | frames ×2, ref images ×7, ref videos ×1 | | `klingai/kling-video-o1-standard` | — | — | — | — | frames ×2, ref images ×7, ref videos ×1 | | `klingai/klingai-1-5-pro` | — | — | 5s, 10s | — | frames ×2 | | `klingai/klingai-1-6-standard` | — | — | 5s, 10s | — | frames ×1 | | `klingai/klingai-2-0-master` | — | — | 5s, 10s | — | frames ×2 | | `klingai/klingai-2-1-master` | — | — | 5s, 10s | — | frames ×1 | | `klingai/klingai-2-1-pro` | — | — | 5s, 10s | — | frames ×2 | | `klingai/klingai-2-1-standard` | — | — | 5s, 10s | — | frames ×1 | | `klingai/klingai-2-5-turbo-pro` | — | — | 5s, 10s | — | frames ×2 | | `klingai/klingai-2-5-turbo-standard` | — | — | 5s, 10s | — | frames ×1 | | `lightricks/ltx-2-19b`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `lightricks/ltx-2-19b-distilled`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `lightricks/ltx-2-fast`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `lightricks/ltx-2-full`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `lightricks/ltx-2.3-fast`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `lightricks/ltx-2.3-full`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `luma/ray3-2` | `720p`, `1080p` | — | 5s, 10s | — | frames ×64 | | `meituan/longcat-video-distilled`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `minimax/hailuo-02` | `512P`, `768P`, `1080P` | — | 6s, 10s | — | — | | `minimax/hailuo-2.3` | `768P`, `1080P` | — | 6s, 10s | — | — | | `minimax/minimax-01-live` | — | — | — | — | frames ×1 | | `minimax/minimax-h3` | — | — | 5–15s | — | frames ×2, ref audio ×3, ref images ×9, ref videos ×3 | | `minimax/video-01-director`
ignores resolution, ratio | — | — | 1–10s | — | — | | `openai/sora-2`
ignores resolution, ratio | — | — | 1–10s | — | — | | `pixverse/pixverse-6-i2v` | — | — | 1–15s | — | — | | `pixverse/pixverse-6-t2v` | — | — | 1–15s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `9:16` | — | | `pixverse/pixverse-c1`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `pixverse/pixverse-lipsync` | — | — | — | — | ref videos ×1 | | `pixverse/pixverse-modify` | `720p` | — | — | — | ref images ×3 | | `pixverse/pixverse-v3-5` | `720p`, `1080p` | — | 5s, 8s | — | frames ×2 | | `pixverse/pixverse-v4` | `720p`, `1080p` | — | 5s, 8s | — | frames ×2 | | `pixverse/pixverse-v4-5` | `720p`, `1080p` | — | 5s, 8s | — | frames ×2 | | `pixverse/pixverse-v5`
ignores resolution, ratio | — | — | 1–10s | — | — | | `pixverse/pixverse-v5-5` | `720p`, `1080p` | — | — | — | frames ×2 | | `pixverse/pixverse-v5-fast` | `720p`, `1080p` | — | 5s, 8s | — | frames ×1 | | `pixverse/pixverse-v5.6`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `prunaai/p-video` | `720p`, `1080p` | — | 1–10s | `16:9`, `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `9:16` | — | | `prunaai/p-video-animate` | `720p`, `1080p` | — | — | — | ref images ×1, ref videos ×1 | | `prunaai/p-video-avatar` | `720p`, `1080p` | — | — | — | frames ×1 | | `prunaai/p-video-replace` | `720p`, `1080p` | — | — | — | ref images ×3 | | `runway/runway-aleph-2-0` | — | — | — | — | frames ×5 | | `runwayml/gen-4-turbo`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `runwayml/gen-4.5`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `skywork/skyreels-v4` | `720p`, `1080p` | — | 3–15s | — | frames ×8, ref images ×3, ref videos ×1 | | `veed/fabric-1.0` | `480p`, `720p` | — | — | — | — | | `veed/fabric-1.0/fast` | `480p`, `720p` | — | — | — | — | | `venice/wan-2.7-uncensored`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `vidu/q1` | `1080p` | — | — | `16:9`, `1:1`, `9:16` | — | | `vidu/q2` | `540p`, `720p`, `1080p` | — | — | `16:9`, `1:1`, `3:4`, `4:3`, `9:16` | — | | `vidu/q2-pro-fast` | `720p`, `1080p` | — | — | — | — | | `vidu/q3` | `540p`, `720p`, `1080p`, `1080p-sr`, `1440p-sr` | — | — | `16:9`, `1:1`, `3:4`, `4:3`, `9:16` | — | | `vidu/q3-pro` | `540p`, `720p`, `1080p`, `1080p-sr`, `1440p-sr` | — | — | `16:9`, `1:1`, `3:4`, `4:3`, `9:16` | — | | `vidu/q3-turbo` | `540p`, `720p`, `1080p`, `1080p-sr`, `1440p-sr` | — | — | `16:9`, `1:1`, `3:4`, `4:3`, `9:16` | — | | `vidu/vidu-q1`
ignores resolution, ratio | — | — | 1–10s | — | — | | `vidu/vidu-q1-classic` | — | — | — | — | frames ×2 | | `vidu/vidu-q3`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `wan-ai/wan2.1-pro-i2v`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `wan-ai/wan2.2-i2v-a14b`
values are provider-wide, not specific to this model | — | `1280x720`, `720x1280`, `960x960` | — | — | — | | `wan-ai/wan2.5-preview`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `wan-ai/wan2.6-flash-i2v`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `wan-ai/wan2.6-t2v` | — | `1080*1920`, `1088*832`, `1248*1632`, `1280*720`, `1440*1440`, `1632*1248`, `1920*1080`, `720*1280`, `832*1088`, `960*960` | 2–15s | — | — | | `wan-ai/wan2.7-i2v` | `720P`, `1080P` | — | 2–15s | — | — | | `wan-ai/wan2.7-r2v` | `720P`, `1080P` | — | 2–15s | `16:9`, `1:1`, `3:4`, `4:3`, `9:16` | — | | `wan-ai/wan2.7-t2v` | `720P`, `1080P` | — | 1–10s | `16:9`, `1:1`, `3:4`, `4:3`, `9:16` | — | | `xai/grok-imagine-1.5-video`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `xai/grok-imagine-video`
values are provider-wide, not specific to this model | `256p`, `360p`, `480p`, `540p`, `580p`, `720p`, `768P`, `1080p`, `true_1080p`, `1440p`, `2K`, `2160p`, `4k`, `1x`, `2x`, `4x` | — | 1–30s | `16:9`, `1:1`, `21:9`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `9:21`, `adaptive`, `auto` | — | | `xai/grok-imagine-video-1-5` | `720p`, `1080p` | — | 1–15s | — | frames ×1, ref images ×7 | 135 models. A dash means the model does not take that parameter, or publishes no value for it. ### What the route dropped A parameter the serving model does not implement is not an error. Mesh takes the request, generates the video without that parameter, and names it in `unsupported_params` on the response to your `POST`: ```json theme={null} { "id": "t-xxxxxxxxxxxxxxxx", "status": "queued", "unsupported_params": ["bgm", "negative_prompt"], "dropped_parameters": [ { "name": "bgm", "reason": "not_supported_by_offering", "offering": "byteplus/dreamina-seedance-2-0" }, { "name": "negative_prompt", "reason": "not_supported_by_offering", "offering": "byteplus/dreamina-seedance-2-0" } ] } ``` `unsupported_params` is a flat array of top-level field names, spelled as you sent them, sorted alphabetically. `dropped_parameters` carries the same drops in the shape chat and image responses use. `offering` is the model the request was graded against, which is the model that served it if a [submit-time fallback](#poll-for-the-result) moved it. `class` (`advisory` or `output_semantic`) appears when Mesh knows it. Read it before concluding a parameter had no effect: a video that ignored your negative prompt and a video whose negative prompt did nothing useful look identical, and this is what tells them apart. Three things about it will catch you out: **It is on the `POST` response only.** Polling, the list endpoint and the webhook payload do not carry either field, and it is not stored, so if you throw the create response away you cannot get it back. **Log it next to the task ID.** **It is `null`, not `[]`, when nothing was dropped.** So is `dropped_parameters`. A truthiness check on `.length` throws. Guard for null. **It names top-level parameters only.** `model` and `content` are excluded — but a `content` item the model cannot take is never silently dropped either. It is refused with a `422` at submit, because a dropped asset changes what gets generated and you would be billed for it: a dropped *tunable* still gives you the video you asked for, tuned differently. Treat a non-empty `unsupported_params` as something to fix rather than something to live with: the request you meant to send and the request that ran are not the same one. ## Getting the result Two ways to collect a finished video: ask for it, or have it delivered. Polling is the source of truth. A callback is a shortcut that can arrive before your next poll, and on Kling and MiniMax it is skipped if a poll (yours or Mesh's own background check) settles the task first. You can do both: usage logging is deduplicated, so you are never billed twice. | Approach | Best for | | :- | :- | | **Webhook** | Production apps and background pipelines — event-driven, no polling overhead | | **Polling** | Scripts, testing, anywhere you cannot expose a public endpoint | ### Poll for the result `GET /v1/video/generations/{id}` returns the task's current state. While a task is still running, Mesh fetches the latest status live before answering; once it has finished, later calls are served straight from Mesh. ```python theme={null} import time import requests TASK_ID = "t-xxxxxxxxxxxxxxxx" headers = {"Authorization": "Bearer rsk_YOUR_KEY"} while True: task = requests.get( f"https://api.meshapi.ai/v1/video/generations/{TASK_ID}", headers=headers, ).json() if task["status"] == "succeeded": print("Video:", task["content"]["video_url"]) break if task["status"] in ("failed", "expired", "cancelled"): print("Did not finish:", task.get("error")) break print("Still", task["status"]) time.sleep(5) ``` ```bash theme={null} while true; do RESULT=$(curl -s "https://api.meshapi.ai/v1/video/generations/$TASK_ID" \ -H "Authorization: Bearer rsk_YOUR_KEY") STATUS=$(echo "$RESULT" | jq -r '.status') echo "Still $STATUS" if [[ "$STATUS" != "queued" && "$STATUS" != "running" ]]; then echo "$RESULT" | jq break fi sleep 5 done ``` A five-second interval is a reasonable place to start. Branch on `status`, never on the HTTP code: a task that failed still returns `200`. Polling a task that has not settled yet triggers a live upstream check, so it needs a key with the `video:write` scope. A `video:read`-only key gets `403` there, and can watch progress through the [list endpoint](#list-your-recent-tasks) instead. An unknown task ID, or one that belongs to another account, returns `404`. If your key's [routing policy](/docs/platform/retry-and-fallback) allows fallback, a submit the first provider rejects can be retried on another provider or model before the task is created. Polling and billing follow the route that accepted it. **You can poll with your own request id instead.** Pin [`X-Mesh-Version: 2026-09`](/docs/reference/api-versioning) or newer, and the same endpoint also accepts the `request_id` Mesh returned when you created the task — useful when that id is already what you logged or stored: ```bash theme={null} curl "https://api.meshapi.ai/v1/video/generations/req_01ARZ3NDEKTSV4RRFFQ69G5FAV" \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -H "X-Mesh-Version: 2026-09" ``` The task id is tried first, so nothing you already poll by changes. From `2026-09` the task body also carries a `request_id` field. Tasks created before 20 August 2026 have no request id recorded and stay reachable by task id only. | Status | Meaning | | :- | :- | | `queued` | Accepted, not started yet | | `running` | Generating | | `succeeded` | Done — `content.video_url` is your video | | `failed` | Did not generate — read `error` | | `expired` | Ran out of time before finishing | | `cancelled` | Cancelled | A finished task looks like this: ```json theme={null} { "id": "t-xxxxxxxxxxxxxxxx", "model": "byteplus/dreamina-seedance-2-0", "status": "succeeded", "content": { "video_url": "https://cdn.example.com/videos/generated.mp4", "last_frame_url": "https://cdn.example.com/videos/last-frame.jpg" }, "usage": { "completion_tokens": 200, "total_tokens": 200 }, "resolution": "1080p", "ratio": "16:9", "duration": 5, "frames": 150, "framespersecond": 30, "billed_resolution": "1080p", "billed_usd": 0.0015, "settled": true, "created_at": 1718000000, "updated_at": 1718000180 } ``` | Field | Description | | :- | :- | | `id` | Task ID | | `model` | The model that ran | | `status` | Current status | | `content.video_url` | Your video, once `succeeded` | | `content.last_frame_url` | Still of the final frame, when you asked for `return_last_frame` | | `error.code` / `error.message` | Why it failed, when it did | | `request_id` | The `req_` id of the request that created the task (from `X-Mesh-Version: 2026-09`) | | `usage.completion_tokens` / `usage.total_tokens` | Tokens billed, on token-billed models only. `null` otherwise | | `billed_seconds` / `billed_resolution` | The length and resolution tier you were charged for, on models priced by duration or resolution | | `billed_usd` | What the task cost, in USD. `null` until it settles | | `settled` | `true` once billing for the task is recorded. Tells "not billed yet" apart from "billed nothing" | | `duration`, `resolution` | What you requested | | `seed`, `ratio`, `frames`, `framespersecond`, `generate_audio` | What the output is. Returned by BytePlus models, and only on a poll that fetched the status live; `null` otherwise | | `created_at` / `updated_at` | Unix timestamps | Copy the video into your own storage when the task succeeds. Result URLs are there for you to fetch the file, not to serve it from your product. ### Have the result delivered Pass a `callback_url` on the create request and Mesh POSTs the task to it when it finishes. Only models served by BytePlus, Kling or MiniMax take a `callback_url`; on any other model the create call returns `422`, and you poll instead. Kling and MiniMax tasks are delivered at most once, on a terminal status (`succeeded`, `failed`, `expired`). If a poll (yours or Mesh's own background check) settles the task first, no callback is sent. BytePlus forwards every status update it sends, so expect `running` deliveries too and act on the terminal one. Treat the callback as a shortcut and polling as the source of truth. ```json theme={null} { "model": "byteplus/dreamina-seedance-2-0", "content": [ { "type": "text", "text": "A neon-lit street in the rain, slow dolly forward" } ], "duration": 5, "callback_url": "https://yourapp.example.com/webhooks/video" } ``` The body is the same shape polling returns, with one difference: null fields are **omitted** rather than sent as `null`. Read a missing key as null, not as an error. To hear about every video task on the account without a per-request URL, subscribe to the `video.completed` and `video.failed` [webhook events](/docs/capabilities/notifications). ```python theme={null} from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/webhooks/video", methods=["POST"]) def video_webhook(): task = request.json status = task["status"] if status == "succeeded": print(task["id"], "->", task["content"]["video_url"]) # copy the file into your own storage here elif status == "failed": print(task["id"], "failed:", task.get("error", {}).get("message")) elif status == "expired": print(task["id"], "expired before finishing") return jsonify({"ok": True}), 200 ``` **Return a `2xx` quickly, and do the heavy work afterwards.** The callback is fire-and-forget with a 10-second timeout and **no retry** — a slow or failing endpoint means you miss that event for good. Poll as a backstop if missing one would matter. ### List your recent tasks `GET /v1/video/generations` returns your tasks, newest first. It is served from Mesh without checking upstream, so an in-progress task can show a stale status — call the single-task endpoint for a live one. ```bash theme={null} curl "https://api.meshapi.ai/v1/video/generations?status=succeeded&limit=10" \ -H "Authorization: Bearer rsk_YOUR_KEY" ``` | Parameter | Description | | :- | :- | | `status` | Filter by status. Repeatable, and multiple values are OR-ed: `?status=running&status=succeeded`. | | `model` | Filter by model ID. Repeatable, OR-ed the same way. | | `created_after` / `created_before` | ISO 8601 timestamps. | | `limit` | Page size, 1–200 (default 50). | | `offset` | Pagination offset (default 0). | ```json theme={null} { "object": "list", "data": [ { "id": "t-xxxxxxxxxxxxxxxx", "model": "byteplus/dreamina-seedance-2-0", "status": "succeeded", "created_at": 1718000000, "updated_at": 1718000180, "content": { "video_url": "https://cdn.example.com/videos/generated.mp4" } } ], "has_more": false, "total": 1, "limit": 50, "offset": 0 } ``` ## What a video costs How a video is billed depends on the model's `pricing.pricing_unit` on `GET /v1/models`: | Pricing unit | How the charge is worked out | | :- | :- | | `per_1m_tokens` | `usage.completion_tokens` on the finished task at the model's video rate: BytePlus Seedance and `google/gemini-omni-flash-preview` | | `per_second` | Output seconds × the rate for the resolution tier, e.g. Kling, DeepInfra, OpenAI, AtlasCloud, most Runware and Veo models | | `per_video` | A flat price per clip, on some models tiered by resolution | Where a model prices them separately, audio (`generate_audio: true`) and video input change the rate, not always upward. On Seedance 2.x, generating from a video input is billed at a *lower* rate per token than video alone. Venice models bill the price Venice quotes for the shape you asked for. `billed_usd` on the finished task is what you were charged, and `billed_resolution` the tier it was charged at. Per-model rates are on the [Pricing](/docs/getting-started/pricing) page, and [Models & Pricing](https://meshapi.ai/models) carries the live numbers. ## When something is refused ### A task that failed after it was accepted The most common surprise here: a video task can be accepted and fail later. The GET returns **HTTP 200** with `status: "failed"` and an `error` object — the failure is in the body, not the status code. ```json theme={null} { "id": "t-xxxxxxxxxxxxxxxx", "status": "failed", "error": { "code": "content_policy_violation", "message": "The request could not be processed due to content policy." } } ``` So always branch on `status`, and log `error.code`. Mesh's own codes are: | `error.code` | Meaning | | :- | :- | | `content_policy_violation` | The provider refused the content. Mesh folds every provider's spelling of a content refusal into this one code | | `video_generation_failed` | The provider reported a failure without giving a reason | | `task_abandoned` | A poll-only provider never reported a terminal status within an hour. Closed and not charged | | `provider_resource_not_found` | The provider no longer has the task, usually purged after its retention window | | `upstream_error` | The provider reported a failure. Kling, Runware, DeepInfra, AtlasCloud, Novita and AI Studio Veo tasks | | `provider_video_failed` | The provider reported a failure. MiniMax, SiliconFlow and fal tasks | | `provider_video_no_file` | MiniMax reported success but returned no file to download. Not charged | | `video_rehost_failed` | The video was generated but Mesh could not store it for delivery. Gemini and AI Studio tasks. Not charged | Any other code is the provider's own, passed through as it sent it; `message` carries the provider's explanation. ### Where to look first | What you saw | Usually means | | :- | :- | | Accepted, then `failed` minutes later | Two [input scenarios](#pick-one-input-scenario-not-two) in one request, or parameters that contradict the [reference job](#say-which-reference-job-you-mean) the model inferred | | A parameter that did nothing | The model does not implement it — check [`unsupported_params`](#what-the-route-dropped) and the [capability flags](#finding-out-what-a-model-accepts) | | A reference image that seems ignored | Screening declined it, or a [subject group](#name-your-subjects) whose main view was not first | | `422` saying a reference is still being prepared | [Registration](#keeping-a-character-consistent) is still in flight — retry the same request | | A plain HTML `413` with no error body | The request body [exceeded 32 MiB](#what-you-can-send) at the edge | | `400` on a prompt-only request | The model needs input media — an image-to-video or upscaling model | | `400` `model_capability_not_supported` naming a resolution | The model cannot render that resolution. Some Runware models refuse tiers outside their own list | | `422` naming `callback_url` | The model's provider has no webhooks. [Poll](#poll-for-the-result) instead | ### HTTP status reference | Status | Meaning | | :- | :- | | `200` | Task created, or retrieved | | `400` | The model needs media this request did not carry, or cannot render the requested resolution | | `401` | Missing or invalid API key | | `402` | Insufficient balance, or a spend cap reached | | `403` | Polling an unsettled task with a key that lacks the `video:write` scope | | `404` | No task with that ID on your account | | `422` | Not a video model, or a `role`, `subject` or parameter value the endpoint rejects | | `429` | Rate limit exceeded | | `502` | Upstream error while generating | | `503` | Video generation temporarily unavailable | `429`, `502` and `503` are usually transient — retry with backoff. For anything else, [Video generation troubleshooting](/debug/video-generation) walks through the specific symptoms, and the [error reference](/docs/reference/errors) covers codes shared across the API. # Web Search Source: https://developers.meshapi.ai/docs/capabilities/web-search Search the live web through a single endpoint — engines, failover, filtering, page content, and per-engine pricing. `POST /v1/web/search` returns live web results through one interface, with automatic failover between engines. Use it to ground a model's answer in current information, or as a standalone search API. This is distinct from the hosted `web_search_preview` tool on the [Responses API](/docs/capabilities/responses-api), which searches *inside* a model turn. This endpoint returns results to your code. *** ## Basic use ```bash theme={null} curl https://api.meshapi.ai/v1/web/search \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "latest James Webb telescope discoveries", "max_results": 5 }' ``` ### Parameters | Field | Default | Notes | | - | - | - | | `query` | required | 1–2,000 characters | | `max_results` | `5` | 1–20 | | `include_answer` | `false` | Ask for a synthesized answer alongside results. Only the Tavily engine returns one | | `include_page_content` | `false` | Also return each result's extracted page text. TinyFish only — see [Page content](#page-content) | | `include_domains` | — | Restrict results to these domains | | `exclude_domains` | — | Drop results from these domains | | `provider` | — | Pin to `native`, `tavily` or `tinyfish`. Omit for automatic failover | | `search_depth` | `basic` | `basic` or `advanced`. Tavily only | | `model` | — | Optional and has no effect. `perplexity/sonar` is the only accepted value; anything else is a `422`. Use `provider` to choose an engine | *** ## Engines and failover By default MeshAPI tries the **native** engine first and falls back to another engine if it fails or returns no results. You get a result without handling engine outages yourself. Pinning `provider` **disables failover**. If you pin an engine and it is down, the request fails rather than trying the other one. Omit `provider` unless you specifically need one engine's behaviour. ```bash theme={null} curl https://api.meshapi.ai/v1/web/search \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "latest James Webb telescope discoveries", "provider": "tinyfish", "max_results": 5 }' ``` | Engine | Notes | | - | - | | `native` | The default. No `answer` and no page content; ignores `search_depth` | | `tinyfish` | Returns one page of results, so `max_results` may yield fewer. Domain filters are applied after the search. No `answer`; ignores `search_depth`. The only engine that returns page content. Lower throughput: under heavy load a pinned request can return `429` | | `tavily` | The only engine that honours `include_answer` and `search_depth`. No page content | *** ## Page content Set `include_page_content: true` and every result carries that page's extracted text in `page_content`, so one call replaces search-then-fetch. ```bash theme={null} curl https://api.meshapi.ai/v1/web/search \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "latest James Webb telescope discoveries", "provider": "tinyfish", "include_page_content": true, "max_results": 2 }' ``` **TinyFish only.** The native and Tavily engines have no equivalent and leave `page_content` empty — documented behaviour, not an error. Native is tried first by default, so an **unpinned request will usually come back with no page text at all**. Pin `provider: "tinyfish"` whenever you need it, and read [Engines and failover](#engines-and-failover) first: pinning also turns failover off. It is off by default because it is **slow, large and billed per page**. * Page content costs \$0.001 per returned result that carries it, on top of the \$0.005 search. * The search takes seconds rather than under two. Measured 3–14 s against 1.6–2.3 s without it; the spread depends on how recently those pages were crawled. * Expect tens of kilobytes per result. The engine fetches **every result it found**, not only the `max_results` you asked for, so asking for fewer results does not make it faster — you simply receive, and pay for, fewer. Ask for it when you will actually read the text. An empty `page_content` on a TinyFish result is normal. Some pages cannot be read, and the engine says so by returning no text for that one result instead of failing the whole search — so handle it per result, not per request. When a page is longer than the per-result limit, the text is cut **and** `page_content_truncated` is `true` on that result. Check it before treating the text as a whole page. *** ## Billing Each successful search is billed at the price of the engine that answered it. The response's `provider` field tells you which engine that was. | Engine | Price per search | | - | - | | `native` | \$0.005 | | `tavily` | \$0.008, or \$0.016 with `search_depth: "advanced"` | | `tinyfish` | \$0.005, plus \$0.001 per returned result with `page_content` | Result count does not change the price. `include_page_content` adds \$0.001 for each returned result that carries page text; pages the engine read but did not return are not charged. Searches that fail or return nothing are **not** charged. Without a pinned `provider`, a search that falls over from `native` is billed at the engine that served it, so a failover to `tavily` costs \$0.008. Pin `provider` if you need a fixed price per call. Web search is priced per call, not per token, so budget a high-volume workload by call count. *** ## Response ```json theme={null} { "query": "latest James Webb telescope discoveries", "answer": null, "results": [ { "title": "JWST spots earliest known galaxy", "url": "https://example.com/jwst", "content": "Astronomers using the James Webb Space Telescope…", "score": 0.93, "published_date": null, "page_content": null, "page_content_truncated": false } ], "provider": "native", "request_id": "req_01J…" } ``` `provider` says which engine served the request. `answer` is set only when you sent `include_answer: true` and Tavily served it. `score` is a rank from 0 to 1; for engines that report no relevance it follows result order. `page_content` and `page_content_truncated` are present on **every** response, from every engine, and read `null` and `false` when you did not ask for page content or the engine had none. They were added alongside the existing fields rather than replacing any, so a client that ignores unknown fields is unaffected — but a client that rejects them needs to allow these two. In particular `content` still holds the short snippet it always did; page text never replaces it. Web search is not a model call, so a key's model allow-list doesn't affect it. Rate limits, spend caps and balance checks still apply. *** ## Related * [Responses API](/docs/capabilities/responses-api) — in-turn hosted web search * [API Keys](/docs/getting-started/api-keys) — limits * [Pricing](/docs/getting-started/pricing) # Commands & permission modes Source: https://developers.meshapi.ai/docs/cli/commands Every Mesh CLI slash command, plus the permission modes that govern what it may run. ## Permission modes The colored bar at the bottom of the screen always shows the current permission mode. Press **Shift+Tab** to cycle through the four modes: | Mode | Color | Behavior | | - | - | - | | **default** | green | Asks before every file write, command, or search. Safest — start here. | | **accept edits on** | cyan | File writes in your current folder happen automatically; commands still ask. | | **auto** | yellow | File writes, commands, and web searches all run automatically. | | **bypass permissions on** | red | Everything runs automatically. | Even in **bypass permissions** mode, genuinely dangerous actions — `rm -rf`, `sudo`, or writing to `~/.ssh` — still stop and ask for confirmation. ## Commands you'll actually use Slash commands work from the prompt inside the CLI: | Command | What it does | | - | - | | `/help` | Lists every command, explained. | | `/model ` | Switch model, with fuzzy suggestions as you type. | | `/models` | Browse the full model catalog with prices. | | `/route auto` | Let Mesh pick the best model for each prompt automatically. | | `/memory` | Shows what the CLI remembers about this project — it learns your repo as it works. | | `/cost` | Total spend for the current session. | | `/update` | Check for a newer version (the CLI also offers upgrades on its own). | | `/login` | Re-enter your API key (use this if you pasted a wrong key). | | `/exit` | Leave the CLI. Next time, just run `meshapi` again. | ## Staying up to date New versions add models, fix bugs, and expand what the CLI can do. You don't reinstall — you update. **Easiest:** the CLI checks for new versions itself. When it offers one, press `y`. To check any time while it's running, type `/update`. **Manual:** quit the CLI with `/exit`, then run this (same on Mac and Windows): ```bash theme={null} pipx upgrade meshapi-code ``` Confirm it worked: ```bash theme={null} meshapi --version ``` Check that the number went up. If it still shows the old version, close the window, open a new one, and check again. Running into errors? See [Mesh CLI debugging](/debug/mesh-cli). # Install the Mesh CLI Source: https://developers.meshapi.ai/docs/cli/install Install the Mesh CLI on macOS, Linux, or Windows and sign in with your API key. The Mesh CLI (`meshapi`) is a terminal app that lets you chat with any model, switch between 1,300+ models, and have it write and run code for you, all from your Terminal or PowerShell. Prefer a desktop app with nothing else to install? [Mesh Code](/docs/mesh-code/install) runs the same agent in a window and needs no Python. It installs with [pipx](https://pipx.pypa.io), which needs Python. The steps below install Python, pipx, and the CLI from scratch. Pick your OS. The whole install is three tools: **Python** → **pipx** → **meshapi-code**. After the one-time setup, updating is a single command — see [Using the CLI → Staying up to date](/docs/cli/commands#staying-up-to-date). ### 1. Install Python 1. Open Safari (or any browser) and go to [python.org/downloads](https://python.org/downloads). 2. Click the yellow **Download Python 3.x** button (any recent 3.x is fine). 3. Open the downloaded file — named like `python-3.14.6-macos11.pkg`. 4. Click **Continue → Continue → Continue → Agree → Install**, enter your Mac login password, then **Install Software**. 5. Wait for *"The installation was successful"* and click **Close**. ### 2. Check Python 1. Press **⌘ + Space**, type `Terminal`, press **Enter**. 2. Run: ```bash theme={null} python3 --version ``` You should see `Python 3.` followed by numbers. ✅ ### 3. Install pipx ```bash theme={null} python3 -m pip install --user pipx python3 -m pipx ensurepath ``` The second command prints something like `Success! Added /Users/you/.local/bin to the PATH`. Now **quit Terminal completely** (**⌘ + Q**) and open a fresh window (**⌘ + Space → Terminal**). The next command only works in a new window that has picked up the updated PATH. ### 4. Install the Mesh CLI ```bash theme={null} pipx install meshapi-code ``` When you see `done!` and `installed package meshapi-code`, the CLI is ready. ✅ ``` done! ✨ 🌟 ✨ installed package meshapi-code 0.5.10 These apps are now available - meshapi ``` ### 1. Install Python 1. Open Microsoft Edge (or any browser) and go to [python.org/downloads/windows](https://python.org/downloads/windows). 2. Click **"Latest Python 3 Release - Python 3.x"** at the top of the list. 3. Scroll to the **Files** table and click **"Windows installer (64-bit)"**. The file downloads. 4. Open the downloaded file — named like `python-3.14.6-amd64.exe`. **The most important step:** on the first installer screen, tick **"Add python.exe to PATH"** at the bottom — do this *before* clicking anything else. If you skip it, the CLI won't be found later. 5. Click **Install Now**, click **Yes** if Windows asks for permission, wait for *"Setup was successful"*, then **Close**. ### 2. Check Python 1. Click **Start**, type `PowerShell`, press **Enter**. 2. Run: ```powershell theme={null} py --version ``` You should see `Python 3.` followed by numbers. ✅ If you get an error, redo Step 1 and make sure the **PATH** checkbox was ticked. ### 3. Install pipx ```powershell theme={null} py -m pip install --user pipx py -m pipx ensurepath ``` The second command prints something like `Success! Added C:\Users\you\.local\bin to the PATH`. Now **close the PowerShell window** (click the X) and open a fresh one (**Start → PowerShell**). The next command only works in a new window that has picked up the updated PATH. ### 4. Install the Mesh CLI ```powershell theme={null} pipx install meshapi-code ``` When you see `done!` and `installed package meshapi-code`, the CLI is ready. ✅ ## Next step The CLI is installed. Head to [Using the Mesh CLI](/docs/cli/usage) to add your API key and send your first prompt. Hit a snag during install? See [Mesh CLI debugging](/debug/mesh-cli). # Using the Mesh CLI Source: https://developers.meshapi.ai/docs/cli/usage Chat, switch models, and run code from your terminal with the Mesh CLI. Once the CLI is [installed](/docs/cli/install), you're one API key away from your first response. Commands are identical on macOS and Windows — the only difference is how you paste (**⌘+V** on Mac, **right-click** in PowerShell). ## 1. Get your API key This is a one-time step. 1. Go to [app.meshapi.ai](https://app.meshapi.ai) and sign in (or create an account). 2. In the left menu, click **API Keys → Create key** and name it anything. 3. Click the copy icon. The key starts with `rsk_`. Keep it private — treat it like a password. ## 2. First launch 1. In Terminal (Mac) or PowerShell (Windows), run: ```bash theme={null} meshapi ``` 2. It asks for your key once. Paste it and press **Enter**. Nothing appears on screen while you paste the key — that's normal, it's hidden for security. 3. You land on the home screen, which shows your **version**, current folder (**cwd**), and the active **model**: ``` + meshapi 0.5.10 cwd: /private/tmp/mesh-demo model: openai/gpt-4o-mini route: off type /help for commands, /exit to quit ``` ## 3. Say hello Type a message and press **Enter**: ``` hello ``` The answer streams in. The status line under every answer shows the **model used**, **tokens in→out**, **cost**, and **time**: ``` Hello! How can I assist you today? gpt-4o-mini · 2024-07-18 · 1979→10 tok · 2.4s ``` ## 4. Switch models Over 1,300 models are available. * Type `/model ` and start typing a name — a suggestion menu pops up as you type (e.g. `qw` shows all Qwen models). Pick with the arrow keys + **Enter**. * Or type it in full: ``` /model openai/gpt-4o-mini ``` * To browse the full catalog with prices, type `/models` (or filter, e.g. `/models claude`). Prices are shown **per 1 million tokens**. Don't want to pick manually? Turn on automatic routing with `/route auto` and Mesh chooses the best model for each prompt. See [Auto Routing](/docs/capabilities/auto-routing). ## 5. Let it write and run code The CLI can create and run files for you. Try: ``` write a hello.py file that prints Hello Mesh, then run it ``` Before touching your computer, the CLI **asks permission** and shows exactly what it wants to do: ``` ○ approve tool call? write_file: hello.py (19 chars) → /private/tmp/mesh-demo/hello.py print('Hello Mesh') y (yes) / a (always for write_file this session) / n (no) › ``` Your three choices: * **`y`** — yes, this once. * **`a`** — yes, and don't ask again for this tool for the rest of the session. * **`n`** — no. Press `y` to write the file, then `y` again when it asks to run it. The CLI runs the file and shows the output: ``` $ python3 hello.py Hello Mesh ✓ exit 0 ``` How much the CLI can do without asking depends on the current **permission mode** — see [Commands & permission modes](/docs/cli/commands#permission-modes). ## Next step Learn the [commands and permission modes](/docs/cli/commands) you'll use every day. # Account Configuration Checklist Source: https://developers.meshapi.ai/docs/getting-started/account-checklist Configure funding, spend caps, keys, rate limits, fallbacks, caching, memory, templates, usage visibility, orgs, and BYOK — with the default for every setting. Use this checklist to set up your Mesh account. It takes about 15 minutes. * Work top to bottom — the order matters. Money first, then keys, then features. * Every item shows the **default if not set**, so you know exactly what happens if you skip it. * Most settings live in the [dashboard](/docs/getting-started/dashboard). API fields are noted for developers. * New to Mesh? Make your first call with the [Quickstart](/docs/getting-started/quickstart), then come back here. *** ## 1. Add money and protect your spend Mesh is prepaid: you add credits, and each request draws them down. See [Pricing](/docs/getting-started/pricing) for how costs work. * [ ] **Add credits and check your balance.** * Every paid request draws from your credit balance. At \$0, requests are rejected with a `402` error. * You are billed after each call completes. So one large request can push your balance slightly below zero — that's normal, and it settles on your next top-up. * **Default if not set:** no credits — paid requests fail with `402`. * **Where:** Dashboard → Billing. API: `GET /v1/balance`. * [ ] **Turn on auto-recharge.** * Tops up your balance automatically when it runs low, so a long job never dies mid-run with a `402`. * Auto-recharge runs on a UPI AutoPay mandate, charged in INR, so it is available only for UPI payments. * **Default if not set:** off — your balance can hit \$0 and requests start failing. * **Recommended:** add \$25 whenever the balance drops below \~\$10. Each recharge can be \$5–\$100, and you can have one active auto-recharge setup per account. * **Where:** Dashboard → Billing → Auto-recharge. It is a dashboard-only setting with no API-key equivalent. * [ ] **Set a spend cap on every key.** * Caps how much a single key can spend. * **Default if not set:** no cap — **unlimited spend** on that key. * **Where:** Dashboard → API Keys. API: `spend_cap_usd` on `POST /v1/keys` or `PATCH /v1/keys/{id}`. * **Good to know:** caps are soft — a key can go over by at most one request before the `402` kicks in. If you use orgs, caps also exist per org, team, and member (over a rolling 30 days). ## 2. Create your API keys Keys are how your apps sign in to Mesh. They are also the unit for limits, caps, and usage tracking. See [Authentication](/docs/getting-started/authentication) and the [Dashboard Guide](/docs/getting-started/dashboard). * [ ] **Create one key per app or environment.** * Don't share one key across apps. Separate keys let you see which app spent what — and cap each one. * The full key (`rsk_…`) is shown **once** at creation. Store it somewhere safe. * **Default if not set:** you have no keys — you need at least one to make calls. * **Where:** Dashboard → API Keys. API: `POST /v1/keys`. * [ ] **Set a default model on each key.** * Your apps can then leave out the `model` field. * If a request does name a model, the request wins. Order: request body → key default → template. Templates never override. * **Default if not set:** no default — requests that omit `model` fail. * **Where:** Dashboard → API Keys. API: `default_model` on the key. * [ ] **Leave rate limits alone unless you need more.** * Limits how many requests a key can make, counted per minute and per day. * **Default if not set:** 100 requests/min and 10,000/day per key. How high you can set it yourself depends on your org's rate-limit tier: 1,000/min and 1,000,000/day on the standard tier. See [Rate Limits & Spend Caps](/docs/getting-started/rate-limits). * Going over returns a `429` error with a `Retry-After` header. * **Where:** Dashboard → API Keys. API: check effective limits with `GET /v1/keys/{id}/limits` (the lowest of key / member / org limits wins) or `GET /v1/usage/rate-limits`. * [ ] **Skip model allow-lists unless you truly need one.** * The `allowed_models` field restricts a key to a fixed list of models. It's easy to get wrong: * `model="auto"` fails whenever the router picks a model that isn't on your list. * **Default if not set:** empty — all models allowed. Recommended for most accounts. * **Where:** API: `allowed_models` on the key. If you do use it, include **every** model your features touch. * [ ] **Optional: set per-model budgets.** * Cap the cost, tokens, or request count for a specific model on a key. These are lifetime totals for that model on the key and never reset. * **Default if not set:** no per-model budgets. * **Where:** API: `model_limits` (`max_cost_usd` / `max_tokens` / `max_requests`) on the key. ## 3. Keep requests reliable Mesh can pick models for you and route around outages. See [Auto Routing](/docs/capabilities/auto-routing). * [ ] **Let Mesh pick the model.** * Set the model to `"auto"` and Mesh picks a good, cost-effective model for each request — and avoids models that are down. * **Default if not set:** requests go to exactly the model you name, with no automatic rerouting. * **Where:** `model: "auto"` in the request body. * [ ] **Or pick from your own shortlist.** * Send a list of models you're happy with, and Mesh returns the best one that's available right now. * **Default if not set:** n/a — this is opt-in per request. * **Where:** API: `POST /v1/router/select` with `candidate_models`. * [ ] **BYOK users: leave fallback on.** * If your own provider key fails (auth or quota errors), Mesh retries on its own credentials so your traffic keeps flowing. * **Default if not set:** on (`allow_fallback: true`). Turn it off only if compliance demands strict BYOK-only — requests then fail hard. * **Where:** `allow_fallback` on your provider key. See [BYOK](/docs/capabilities/byok). * [ ] **Check the retry and failover policy.** * Automatic retries on the same provider, then failover to the same model on another provider, plus optional alternate models. * **Default if not set:** on. Every key gets the System default: up to 2 retries, then cross-provider fallback, within 3 attempts. While another provider is left to try, one of the 3 attempts is held for that fallback. * **Where:** Dashboard → API Keys → edit a key → Resilience, or a shared template under Dashboard → Routing Policies. API: `routing_policy` on the key. See [Retry & Fallback](/docs/platform/retry-and-fallback). ## 4. Cut costs with caching * [ ] **Use the built-in response cache — repeat answers are free.** * Send the same request twice within 24 hours and the second response costs **\$0**. You'll see the header `X-Cache: HIT`. * To qualify: `temperature: 0` (or leave it out) and no `tools` in the request. * The cache is private to your account — your entries are never shared. Entries last 24 hours. * **Default if not set:** on — you get this automatically whenever a request qualifies. * **Opt out per request:** `"cache": false` in the body, or the `X-Mesh-Cache: no-store` header. * [ ] **Cache big, stable prompts at the provider.** * On supported models (for example Anthropic), mark long system prompts with `cache_control`. Cached input then bills at a discounted cache-read rate. * **Default if not set:** no provider caching — you pay the full input price every time. * **Where:** `cache_control` markers in the request. Discounted cache rates are listed per model on `GET /v1/models`. * [ ] **Batch anything that isn't urgent.** * Batch jobs run at cheaper batch pricing where the model supports it. See [Batch API](/docs/capabilities/batch-api). * **Default if not set:** everything runs (and bills) at normal real-time pricing. * **Where:** API: `POST /v1/batches`. ## 5. Memory (optional) Skip this section if you don't use Mesh Memory. * [ ] **Create memories, then attach them to requests.** * Attach with the `x-mem-id` header: comma-separated names or IDs, up to 16 per request. If entries conflict, the one listed first wins. * **Default if not set:** no memory — requests only know what's in them. * **Where:** API: `/v1/memories` and `/v1/memories/{slug}/items`. * **Billing:** injected memory text bills like normal prompt tokens (capped at roughly 1,500 tokens per request). Each lookup also adds a small embedding charge (`memory_search_embed`). * [ ] **Use the right type for each memory item.** * `guardrail` — hard rules, always included word-for-word. * `preference` — stable style or configuration choices. * `fact` — long-tail knowledge; the 8 most relevant facts are pulled in per request. * **Default if not set:** n/a — you choose a type when adding each item. ## 6. Prompt templates * [ ] **Use templates for prompts your team shares.** * A template stores messages and `{{variables}}` once, so every app uses the same prompt. Pick one per request with the `template` field (name or ID). * **Default if not set:** no template — each request carries its own full prompt. * **Where:** Dashboard → Prompts. API: `template` field on the request. * **Good to know:** a template's model and params are only **suggestions**. Both the request and the key defaults override them — don't use a template to enforce a model. * **Learn more:** [Prompt Templates](/docs/capabilities/prompt-templates). ## 7. Watch your usage See [Pricing](/docs/getting-started/pricing) for how costs are calculated. * [ ] **Check your spend regularly.** * **Default if not set:** usage tracking is always on — there's nothing to enable. This step is about knowing where to look. * **Where:** the sidebar's Dashboard page (spend, requests, and a per-model breakdown), Dashboard → Billing (balance and spend history), Dashboard → Logs (per-request detail, with CSV export for finance), and Dashboard → Settings → Usage (org totals and spend by team and member). API: `GET /v1/balance` (live balance), `POST /v1/usage` (aggregates), `POST /v1/usage/events` (per-request history), `GET /v1/usage/spend-trend`. * [ ] **Know how a request is priced.** * Cost = input rate × prompt tokens + output rate × completion tokens, plus per-call fees where shown. * Every model's rates are public on `GET /v1/models` (and Dashboard → Models) — including cache read/write, long-context, and image/audio rates, and any flat per-request fee. * Web search bills a flat **\$0.005 per successful search**. Failed or empty searches are free. * **Default if not set:** n/a — these rates apply to every account. ## 8. Teams & orgs (for companies) * [ ] **Name and configure your org.** * Signup creates an org for you (named after you, with a default team called General). You own it, and you can own only one org, so rename and configure this one rather than creating another. * **Default if not set:** the org from signup, with you as the only member. * **Where:** Dashboard → Settings. Org settings are managed by a signed-in person. * [ ] **Invite your teammates.** * Roles: `admin`, `member`, or `viewer` (read-only), plus any custom roles. * **Default if not set:** it's just you. * **Where:** Dashboard → Settings → People, which also lists the invitations you have sent and not yet had accepted. Settings → Invitations shows invitations you have received. * [ ] **Give each team its own keys.** * Create keys tied to a team so usage and limits count against the right team. * **Default if not set:** keys stay personal — usage isn't attributed to any team. * **Where:** API: `POST /v1/keys` with `org_id` and `team_id`. * [ ] **Set limits per org, team, or member if you need internal cost control.** * Requests/min, requests/day, tokens/min, and 30-day spend caps at each level. The lowest applicable limit wins. * **Default if not set:** **all unlimited.** * **Where:** Dashboard → Settings → Limits. ## 9. Bring your own provider keys (BYOK) Full guide: [BYOK](/docs/capabilities/byok). * [ ] **Decide if you need BYOK at all.** * Use it when you want your own provider quota or negotiated rates, data-residency control, or provider-side features tied to your own account. * **Default if not set:** requests run on Mesh's provider credentials — the right choice for most accounts. * **Pricing:** BYOK traffic carries a platform fee of 5% of the request's cost at Mesh's list price, with the first 1M BYOK tokens each month fee-free. The fee counts toward your spend caps. * [ ] **Add and test your provider keys.** * Supported providers: AWS Bedrock, Google Vertex AI, OpenAI, and Azure AI Foundry, plus pasted API keys for Anthropic, Arcee, Cerebras, Cohere, DeepSeek, Fireworks, Moonshot, Parasail, Perplexity, Qwen, and Sarvam. Every key is checked against the provider before it is saved. * Your secrets are stored in a dedicated secret manager, never in the database. * **Default if not set:** n/a — BYOK is opt-in. * **Where:** Dashboard → BYOK. Provider keys are a dashboard-only setting. * [ ] **Leave fallback on** (`allow_fallback: true`) — see section 3. * **Default if not set:** on. *** ## Defaults if you don't opt in | Setting | Default | Ceiling / bound | | - | - | - | | Rate limit (per key) | 100 RPM / 10,000 RPD | per tier — 1,000 RPM / 1M RPD on standard | | Free-model traffic | 20 RPM / 200 RPD (shared) | — | | Spend cap (key) | **none — unlimited** | set it! | | Org/team/member limits | unlimited | set per tier | | Auto-recharge | off | \$5–\$100 per recharge | | Response cache | on, 24h TTL, hits free | needs `temperature: 0`, no tools | | `allowed_models` | all models | include `perplexity/sonar` + auto-router picks if set | | Retry & cross-provider fallback | on: up to 2 retries, 3 attempts, one held for fallback | `max_retries` ≤ 6, `max_attempts` ≤ 6 | | BYOK fallback | on | — | | Web search | \$0.005/successful search | 100 RPM / 10k RPD | ## Common surprises 1. **No spend cap means unlimited spend.** Set `spend_cap_usd` on every key. 2. **`allowed_models` quietly breaks web search and `model="auto"`** unless the list includes what those features actually use. 3. **A non-zero `temperature` or any `tools` skips the free cache.** No error — you just never see `X-Cache: HIT`. 4. **Templates never win.** The request and the key defaults both override a template's model and params. 5. **Your balance can dip below zero** on the last request, and a spend cap can go over by one request. Both are by design — you're billed after each call completes. 6. **Deleting a key suspends it** (`DELETE /v1/keys/{id}`) — support can restore it. It is not gone forever. ## Need help? * **Include the request ID.** Every API response carries a request ID (`req_…`). Include it when you report an issue — it makes debugging fast. * **Check the dashboard first.** For spend or limit questions, the usage and balance pages usually have the answer. * **Talk to your Mesh contact.** For anything beyond self-serve, such as rate limits above your tier or billing questions, reach out to your Mesh contact or account manager. # Admin Keys Source: https://developers.meshapi.ai/docs/getting-started/admin-keys The mak_ credential a script uses to manage your organization — permissions, scopes, expiry, rotation, and revocation. An admin key (`mak_...`) lets a script manage your organization — creating API keys, reading resolved limits, configuring alerts — without a person signing in. It is deliberately **not** the key you already have. | | `rsk_...` | `mak_...` | | - | - | - | | Calls models, bills your balance | Yes | No | | Manages keys, limits, alerts | No | Yes | | Expiry | Lives until you suspend it | Always expires — up to 90 days | The separation is enforced twice server-side, by credential prefix and by separate storage. A leaked inference key cannot mint more keys, and a leaked admin key cannot spend your balance. Admin keys are minted from the dashboard by a signed-in person, in **Dashboard → Settings → Admin keys**. An admin key cannot mint another admin key, by design. *** ## What an admin key can reach A key carries an explicit permission set. Anything not granted is a `403`, and anything not in this table is out of reach for **every** admin key: | Permission | Reaches | | - | - | | `keys:read` | `GET /v1/keys`, `GET /v1/keys/filter-options`, `GET /v1/keys/{id}/limits` | | `keys:write` | `POST /v1/keys`, `PATCH /v1/keys/{id}`, `DELETE /v1/keys/{id}` | | `limits:read` | `GET /v1/routing-policy-templates`, `GET /v1/routing-policy-templates/{id}` | | `limits:write` | `PATCH` / `DELETE /v1/routing-policy-templates/{id}` | | `alerts:read` | `GET /alerts`, and the webhook delivery log: `GET /v1/webhooks/deliveries`, `/stats`, `/{delivery_id}` | | `alerts:write` | Alert channels and policies under `/alerts`, plus `POST /v1/webhooks/deliveries/test` and `/{delivery_id}/redeliver` | | `org:read` | `GET /v1/orgs/current/permissions/catalogue` | Not reachable with an admin key, at any permission: **inference** (`/v1/chat/completions` and every other model call), **billing and payments**, **org governance** (invites, roles, teams, ownership), **usage and audit reads** — those take an `rsk_` key — and **minting or rotating admin keys**. Creating a routing-policy template is also dashboard-only; an admin key can read and edit templates but not create the first one. ## Scope Scope decides *whose* rows a permission reaches: | Scope | Reaches | | - | - | | `self` | Only rows the key itself owns | | `team` | One team — `team_id` is required | | `org` | The whole organization | `alerts:*`, `limits:*`, and `org:read` act on the organization itself, so a narrower scope would select the same rows while reading as a restriction. They can only be granted at scope `org` — asking for them at `self` or `team` is a `422`. Only `keys:read` and `keys:write` are meaningful below org scope. You can grant only what you already hold: a permission you lack is a `403`, and so is a scope that reaches further than yours. A team admin can mint a `team`-scoped key for their own team, not for another. *** ## Creating one Create one in **Dashboard → Settings → Admin keys**. The form maps onto `POST /v1/admin-keys`, which authenticates the signed-in person. No credential can mint an admin key, so this step is always a human one: ```json theme={null} { "name": "ci-key-provisioner", "permissions": ["keys:read", "keys:write"], "scope": "org", "expires_at": "2026-11-01T00:00:00Z" } ``` | Field | Notes | | - | - | | `name` | Required, non-blank. Shown in listings and on the audit trail | | `permissions` | At least one, from the table above | | `scope` | `self`, `team`, or `org` | | `team_id` | Required when `scope` is `team`, and rejected otherwise | | `expires_at` | **Required** — must carry a UTC offset, be in the future, and be at most **90 days** out. There is no default and no non-expiring admin key | The plaintext key is shown **once**, in the `key` field of the response. Every later read gives you `masked_key` (`mak_*****7f3a`) instead — only a hash is stored. Copy it straight into your secret store. ## Using it The same `Authorization` header your API key uses: ```bash theme={null} curl https://api.meshapi.ai/v1/keys \ -H "Authorization: Bearer $MESH_ADMIN_KEY" ``` Only the management routes above accept it, and they accept nothing else on that plane. *** ## What the keys it creates belong to An inference key minted by an admin key is owned by the **admin key's lineage**, not by a person. That has three consequences worth knowing before you automate anything: * It lands in the admin key's own organization. Sending a different `org_id` is a `403`; sending a different `team_id` works only if the key's scope reaches that team, and a `self`-scoped key can only create in the org's default team. * It bills the organization's shared balance, like any other org key — see [Organizations & Teams](/docs/getting-started/organizations). * It survives rotation of the admin key that created it, because the lineage is what owns it. Replacing an admin key by minting a fresh one instead does **not** carry those keys over. ## Rotating Rotate from **Dashboard → Settings → Admin keys** (`POST /v1/admin-keys/{key_id}/rotate`, optionally with `{"grace_hours": 24}`). Like minting, it is a signed-in action; a key cannot rotate itself. Rotation mints a successor and shortens the predecessor to a grace window — **both keys work during the overlap**, which is what lets you redeploy without a gap. `grace_hours` defaults to 24 and is capped at 7 days. The successor inherits the predecessor's name, permissions, scope, and team. It also inherits its **lifetime**, not the 90-day ceiling: rotating a deliberately short-lived key does not quietly turn it into a long-lived one. Pass `expires_at` to set a different one, subject to the same 90-day rule. ## Revoking Revoke from **Dashboard → Settings → Admin keys** (`DELETE /v1/admin-keys/{key_id}`, optionally with a `reason`). Revocation is **immediate and has no grace window** — that is the difference from rotation. `reason` is optional and is recorded on the key and in the audit trail. ## Listing `GET /v1/admin-keys` and `GET /v1/admin-keys/{key_id}` return the masked key, permissions, scope, expiry, `last_used_at`, and who created it — never the plaintext. What you see depends on your own scope: org-wide access lists every key in the org, team access lists your team's keys plus your own, and anything narrower lists only the keys you created yourself. *** ## Errors | Status | When | | - | - | | `401` | The key is revoked, expired, or unknown — a dead credential reads differently to an operator than a permissions failure | | `403` | Permission not granted, scope too narrow, another organization, or no admin authority over the named team | | `404` | The key exists but your scope cannot see it | | `409` | Already revoked, or a stored grant that can no longer be reproduced on rotation — mint a replacement instead | | `422` | `expires_at` missing an offset, in the past, or past 90 days; blank `name`; `team_id` without `scope: "team"`; an org-wide permission below org scope | ## Related * [Authentication](/docs/getting-started/authentication) — `rsk_` vs `mak_`, and using a key to make requests * [API Keys](/docs/getting-started/api-keys) — the management API this credential is for * [Rate Limits & Spend Caps](/docs/getting-started/rate-limits) — resolving limits across tiers * [Organizations & Teams](/docs/getting-started/organizations) — shared billing, teams, and what stays a dashboard action # API Keys Source: https://developers.meshapi.ai/docs/getting-started/api-keys Create and manage inference keys programmatically — labels, limits, spend caps, model allow-lists, and org/team assignment. An API key (`rsk_...`) is how your applications authenticate to MeshAPI. It is also the unit that carries **limits, spend caps, model access, and usage attribution** — so managing keys is how you control what each application is allowed to do. This page covers the key-management API. For *using* a key to authenticate a request, see [Authentication](/docs/getting-started/authentication). Key management takes an **admin key** (`mak_...`), not an `rsk_` key. An inference key cannot create or modify keys, by design: a leaked key must not be able to mint more. Create an admin key in **Dashboard → Settings → Admin keys**. It is shown once, carries an explicit permission set, and always expires. See [Admin Keys](/docs/getting-started/admin-keys) for permissions, scopes, rotation, and revocation. Everything on this page is also available in **Dashboard → API Keys** without any credential; the API is for automating it. *** ## Creating a key ```bash theme={null} curl https://api.meshapi.ai/v1/keys \ -H "Authorization: Bearer $MESH_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ "label": "production-web", "rpm": 300, "spend_cap_usd": "50.00", "allowed_models": ["openai/gpt-4o-mini", "anthropic/claude-sonnet-4.5"] }' ``` The plaintext key is returned **once**, in the `key` field of the create response, and is never retrievable again — only a hash is stored. Capture it at creation or you will have to issue a new one. ### Fields | Field | Purpose | | - | - | | `label` | Human name shown in the dashboard and usage breakdowns | | `default_model` | Model used when a request omits `model` | | `rpm` / `rpd` / `tpm` | Rate limits — see [Rate Limits & Spend Caps](/docs/getting-started/rate-limits) | | `spend_cap_usd` | Cumulative USD ceiling for this key | | `allowed_models` | Allow-list of models this key may call: exact IDs or a brand wildcard such as `openai/*`, up to 200 entries | | `blocked_models` | Models this key may not call, in the same format. Applied after `allowed_models`; a block always wins | | `model_limits` | Per-model lifetime budgets: `max_cost_usd`, `max_tokens`, `max_requests` per model ID | | `fixed_provider` | Serve this key only from the named provider. No cross-provider fallback, and a model that provider cannot serve is a `400` | | `pii_policy` | The key's PII redaction policy (`{"enabled": true, ...}`), masking matched categories before the request reaches the model. Omit to inherit the org default; `{"enabled": false}` exempts the key | | `body_log_policy` | Keep this key's request and response bodies readable on `GET /v1/usage/requests/{request_id}`. Omit to inherit the org default; refused with `422` while the org has zero data retention on | | `org_id` / `team_id` | Attach the key to an org or team for pooled billing and limits | | `routing_policy` / `routing_policy_template_id` | Retry and fallback behaviour (mutually exclusive — sending both is a `422`) | `rpm` or `rpd` above your organization's rate-limit tier ceiling (1,000 RPM and 1,000,000 RPD on the standard tier) are rejected with `422`. See [Rate Limits & Spend Caps](/docs/getting-started/rate-limits#defaults). A key created with an admin key carries every API scope except `files:read` and `files:write`, so it gets `403` on the [Files & RAG](/docs/capabilities/rag) routes. Create keys that need RAG in **Dashboard → API Keys**. *** ## Managing keys | Operation | Endpoint | | - | - | | List keys | `GET /v1/keys` | | Filter options for the list | `GET /v1/keys/filter-options` | | Update a key | `PATCH /v1/keys/{id}` | | Suspend a key | `DELETE /v1/keys/{id}` | | Resolved effective limits | `GET /v1/keys/{id}/limits` | `DELETE` is a **soft delete**: it sets the key's status to `suspended`. The key stops working at the inference layer immediately, but the record and its usage history are preserved. There is no hard-delete endpoint — this keeps historical usage attributable. *** ## Pinning the API version A key can carry an `api_version` — the dated contract every request made with it receives, unless the request sends its own `X-Mesh-Version` header. Useful when the calling code is not yours to change, or when a whole integration should sit on one version. ```bash theme={null} curl -X PATCH https://api.meshapi.ai/v1/keys/{id} \ -H "Authorization: Bearer $MESH_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"api_version": "2026-08"}' ``` Send `null` to clear it. In the dashboard it is the key's **API Version** field. See [API versioning](/docs/reference/api-versioning) for what a version covers and which ones are served. *** ## Model allow-lists `allowed_models` restricts a key to a named set of models. It is the cleanest way to stop a cheap application reaching an expensive model. An allow-list silently breaks features that route to models you didn't list: * **`model: "auto"`** — the Auto Router picks from your allow-list. If none of its candidates are listed, routing fails. The failure is not obvious from the error. If a feature stops working right after you set an allow-list, this is why. Include the models those features depend on, or leave the key unrestricted. *** ## Org and team keys Assigning `org_id` or `team_id` makes a key part of a shared structure: it draws on the **org's shared balance** and inherits the org's and team's limits, resolved minimum-wins alongside the key's own. Attaching a `routing_policy_template_id` requires the key to belong to an org — templates are org-scoped, and a key with no org gets a `422`. See [Organizations & Teams](/docs/getting-started/organizations) for the surrounding model. *** ## Related * [Authentication](/docs/getting-started/authentication) — `rsk_` vs `mak_`, and using a key to make requests * [Admin Keys](/docs/getting-started/admin-keys) — the `mak_` credential this page's API takes * [Rate Limits & Spend Caps](/docs/getting-started/rate-limits) — how limits resolve across tiers * [API Versioning](/docs/reference/api-versioning) — pinning a key to a dated contract * [Organizations & Teams](/docs/getting-started/organizations) — shared billing and pooled limits * [Account Configuration Checklist](/docs/getting-started/account-checklist) — recommended setup pass # Authentication Source: https://developers.meshapi.ai/docs/getting-started/authentication Securely authenticate your requests to the Mesh API. Mesh API uses standard HTTP `Authorization` headers with a **Router Service Key (RSK)**. All requests must be made over HTTPS. ## Key Types ### Router Service Keys (`rsk_...`) Your primary method of interacting with the API. These are the keys you create in the dashboard. ``` Authorization: Bearer rsk_ ``` **What they control:** * Access to all inference endpoints (`/v1/chat/completions`, `/v1/embeddings`, etc.) * Per-key **spend caps** — set a maximum USD limit to prevent cost overruns * Per-key **rate limits** — configurable Requests Per Minute (RPM), Requests Per Day (RPD), and Tokens Per Minute (TPM) * Optional **default model** — a fallback model used when none is specified in the request ### Admin Keys (`mak_...`) The credential for **managing** your organisation from a script: creating and updating API keys, reading resolved limits, configuring alerts. Create one in **Dashboard → Settings → Admin keys**; the plaintext is shown once. ``` Authorization: Bearer mak_ ``` The two credentials never cross, and that is the point — a leaked inference key must not be able to mint more keys. | | `rsk_...` | `mak_...` | | - | - | - | | Calls models, bills your balance | Yes | No | | Manages keys, limits, alerts | No | Yes | | Expiry | Lives until you suspend it | Always expires — up to 90 days | An admin key carries an explicit permission set (`keys:read`, `keys:write`, `org:read`, `limits:read`, `limits:write`, `alerts:read`, `alerts:write`) and a scope (`self`, `team`, or `org`) — you can grant only what you hold yourself. Rotating one mints a successor and leaves the predecessor valid for a short grace window; revoking takes effect immediately, with no grace. See [Admin Keys](/docs/getting-started/admin-keys) for the full reference. Inviting members, changing roles, and transferring ownership are **not** on the admin-key surface. Those are dashboard actions performed by a signed-in person, by design. ### Provider Keys (BYOK) If you supply your own API keys for upstream providers (OpenAI, Anthropic, AWS Bedrock, Google Vertex AI, Azure AI Foundry, and other pasted-key providers such as DeepSeek or Fireworks), Mesh securely stores and uses them on your behalf. You never reference these directly in your API calls — Mesh handles routing transparently. See [Bring Your Own Keys](/docs/capabilities/byok) for setup instructions. *** ## Security Best Practices Never expose `rsk_` keys in client-side code — browsers, mobile apps, or public repositories. Always proxy through your backend. **1. Set spend caps** on every key. This limits your blast radius if a key is compromised — the attacker can only spend up to your cap. **2. Use environment variables** — never hard-code keys in source files. ```bash theme={null} # .env MESH_API_KEY=rsk_... MESH_ADMIN_KEY=mak_... # only where a script manages the org ``` **3. Rotate immediately** if you suspect a leak. Deactivate the compromised key in the dashboard and generate a new one. Old keys are invalidated instantly. **4. Use separate keys per environment** — one for development, one for staging, one for production. This gives you clean audit trails and granular rate limiting. **5. Monitor per-key usage** in the dashboard **Logs** section. Unexpected spikes often signal misuse before your spend cap triggers. *** ## Using the key All examples in this documentation use `rsk_YOUR_KEY` as a placeholder. Replace it with your actual key: ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-4o-mini", "messages": [{"role": "user", "content": "Hi"}]}' ``` ```python theme={null} import os from openai import OpenAI client = OpenAI( api_key=os.environ["MESH_API_KEY"], base_url="https://api.meshapi.ai/v1", ) ``` ```typescript theme={null} import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.MESH_API_KEY, baseURL: "https://api.meshapi.ai/v1", }); ``` # Dashboard Source: https://developers.meshapi.ai/docs/getting-started/dashboard A guided tour of the Mesh API control panel. The [Mesh Dashboard](https://app.meshapi.ai) is your control center for managing API keys, billing, usage logs, and prompt templates. This page walks through each section. *** ## API Keys Create and manage your `rsk_...` credentials. * **Create keys** with descriptive labels (e.g. `production`, `staging`, `teammate-alice`) * **Set spend caps** — a USD limit per key to prevent runaway costs * **Configure rate limits** — Requests Per Minute (RPM), Requests Per Day (RPD), and Tokens Per Minute (TPM) * **Monitor per-key metrics** — request counts, success/error rates, and total spend ### Key creation options When creating a key you can configure global limits that apply to all models, and optionally restrict which models the key can use with per-model overrides: **Global limits (apply across all models on this key):** | Setting | Description | | - | - | | RPM | Maximum requests per minute | | RPD | Maximum requests per day | | TPM | Maximum tokens per minute | | Spend cap | USD ceiling. Requests are rejected once this is reached. It is a soft cap: the request that crosses it still completes | **Model allowlist (optional):** Select specific models this key is permitted to use. If no models are selected, the key can access all models available to your account. **Per-model limits (optional):** For each model added to the allowlist you can set budgets that apply only when that model is called. They are lifetime totals for that model on this key and never reset: | Setting | Description | | - | - | | Max cost | Per-model USD ceiling | | Max tokens | Per-model total tokens | | Max requests | Per-model request count | Per-model budgets are enforced in addition to the global key limits, and whichever is hit first takes effect. A reached budget returns `402`. Set a spend cap on every key before sharing it or deploying to production. *** ## Billing Manage your pre-paid credit balance. * **Current balance** — real-time view of remaining credits in USD * **Top up** — add credits via Stripe or local payment gateways (UPI, cards) * **Spend history** — detailed breakdown by provider, model, and date range, for as far back as your account goes * **Auto-recharge** — automatically top up when your balance drops below a threshold *** ## Logs Every request through your account is recorded. * **Filter** by date, model, API key, or status (success / error) * **Latency tracking** — time-to-first-token and total response time per request * **Token counts** — prompt and completion token breakdown per call * **Request ID** — a unique `req_...` ID on every request, useful for support tickets **How far back this screen goes depends on the tab.** * **Served** requests come from the same usage records the Dashboard and Billing read, so this tab goes back as far as your account does. Every range on the picker is real here, *All time* included. * **Refused** requests are available here for **up to 90 days**. * **Full detail for one request** — its headers, and its request and response bodies — is available for **up to 90 days**. Opening a request older than that returns `404`. * **Prompts and completions** appear only when the key that served the request had body logging on. That setting controls what is *returned* to you, not what we store, so turning it on does not make older requests readable. 90 days is the furthest these reach, not a guarantee for every day inside it. Today they reach back to 6 August 2026, and that window grows until it meets the cap in early November 2026. So a question about last quarter's traffic is answerable from the Served tab; the full detail behind those requests is not. If you need the bodies for longer, capture them while they are in the window — the surrounding metadata needs no capturing, because it stays available for the life of the account. See [How far back data goes](/docs/reference/usage-api#how-far-back-data-goes). *** ## Models Browse the full model catalog. * **Discover** all enabled models across OpenAI, Anthropic, Google, Meta, and more * **Compare pricing** — live prompt and completion cost per 1M tokens * **Filter free models** — find \$0-cost models for testing and prototyping * **Check capabilities** — context length, supported modalities (text, image, audio) *** ## Prompts **My Prompts** is your org's editable library and **Global Prompts** the curated read-only catalog. Create and manage [Prompt Templates](/docs/capabilities/prompt-templates) through a visual editor. * **Write system prompts** with `{{variable}}` slots for dynamic values * **Set default parameters** — temperature, max tokens, stop sequences * **Test in-dashboard** before deploying to production * **Manage versions** — update templates without touching client code *** ## Usage **Dashboard**, the first item in the sidebar, shows your traffic for a time range. Filter it by API key or model. Every range the picker offers is real here, including *Year to date* and *All time*: usage and spend are billing records and are kept for the life of the account. The 90-day caps are narrower than this screen — they apply to refused requests and to stored request detail, both on [Logs](#logs). * **Totals** — spend, requests, tokens, success rate with the error count, and average cost per request * **By model** — top models by spend, provider share, and a per-model table of requests, tokens, and spend * **Spend trend** — spend over time with a moving average * **Rate limits** — live RPM, RPD, and TPM against your limits **Settings → Usage** is the org admin view: org-wide request, token, and cost totals, plus spend by team and by member. *** ## BYOK Configure your own upstream API keys for providers like AWS Bedrock, Google Vertex AI, Azure AI Foundry, and OpenAI. See [Bring Your Own Keys](/docs/capabilities/byok). *** ## Other sections * **Get Started** — onboarding steps for a new account * **Briefing** — findings, spend, and reliability views over your traffic (rolling out; not on every account yet) * **Routing Policies** — reusable retry and fallback templates; see [Retry & Fallback](/docs/platform/retry-and-fallback#per-key-routing-policy) * **Memory** and **RAG** — browse stored [memories](/docs/capabilities/memory) and manage [files for retrieval](/docs/capabilities/rag) * **MCP** — connection details and the tool catalogue for Mesh's MCP server * **Playground** — try models interactively in **Chat**, **Realtime**, and **Image** * **Notifications** — webhook and email destinations for account events; see [Notifications](/docs/capabilities/notifications) * **Settings** — org administration in tabs: People, Teams, Roles, Admin keys, Invitations, Limits, Model access, Data retention, Request bodies, Data protection, API contract, Usage, and Audit log. Which tabs you see depends on your role # Organizations & Access Control Source: https://developers.meshapi.ai/docs/getting-started/organizations Manage teams, roles, and usage limits across your organization. Mesh API organizes everything under a tenant hierarchy so teams can share billing, set usage limits, and control who can do what. You manage all of this from the [Dashboard](https://app.meshapi.ai). ## The hierarchy ``` Organization └── Team └── Members ``` | Level | What it is | | - | - | | **Organization** | The top-level account. Holds billing, members, and teams. | | **Team** | A group within the organization with its own members, API keys, and usage limits. | | **Member** | An individual person who belongs to the organization, and optionally to one or more teams. | A user can belong to **several organizations** and switches between them in the dashboard; `GET /v1/orgs` lists every membership. You can **own** only one organization at a time. ## Roles ### Organization roles | Role | Capabilities | | - | - | | **Owner** | Full administrative control over the organization. There is exactly one owner per organization. | | **Admin** | Edit organization settings and limits, create and delete teams, invite and remove members, change member roles and limits, and view all members, usage, and spend. | | **Member** | Use the API, manage their own API keys, and view their own usage. Members cannot manage other members, teams, or settings. | | **Viewer** | Read-only across the organization: settings, teams, limits, API keys, members, usage, and billing. Viewers cannot change anything or spend, and cannot see BYOK provider keys. | On top of a built-in role, an owner or admin can grant **custom roles**, named permission sets created under **Settings → Roles**. A custom role adds permissions; it never removes what the built-in role grants, and you can only grant authority you hold yourself. ### Team roles | Role | Capabilities | | - | - | | **Admin** | Manage the team's members, API keys, and usage limits. | | **Member** | Use the team's API keys to make requests. | | **Viewer** | Read the team's API keys and usage without changing them. | Organization owners and admins automatically have admin access to every team in the organization. ## Inviting members An organization owner or admin invites a teammate by email from the Dashboard, choosing their role (admin, member, or viewer) and any custom roles and, optionally, a team to add them to. The invited person receives an email with a link. After signing in with the invited email address, they accept the invitation to join the organization with the assigned role. Admins can view, resend, or revoke pending invitations from the Dashboard. Invitations expire automatically after 72 hours. ## Usage limits & spend caps Limits can be set at five scopes. Each scope supports the same set of controls: | Control | Description | | - | - | | **RPM** | Requests per minute | | **RPD** | Requests per day | | **TPM** | Tokens per minute | | **Spend cap** | Maximum USD spend | The five scopes: | Scope | Applies to | | - | - | | **Organization** | All usage across the entire organization | | **Team** | All usage under a team | | **Org member** | An individual member's usage across the organization | | **Team member** | An individual member's usage within one team | | **API key** | A single key (see [Dashboard → API Keys](/docs/getting-started/dashboard)) | A request must satisfy **every** limit that applies to it — across the key, the member, the team, and the organization. If any one of them is exceeded, the request is rejected. Any scope left unset has no limit at that level. Spend caps at the organization, team, and member levels are evaluated over a rolling 30-day window. ## When a limit is hit | Condition | HTTP status | Error code | | - | - | - | | RPM, RPD, or TPM limit exceeded | `429 Too Many Requests` | `rate_limit_exceeded` | | Spend cap reached | `402 Payment Required` | `spend_limit_exceeded` | Rate-limit responses include a `Retry-After` header indicating how long to wait before retrying. If a member or organization spend cap is reached, contact your organization admin to raise it. *** ## Managing orgs through the API Org governance — invites, roles, teams, ownership — is a **dashboard** surface. Those endpoints authenticate a signed-in person and accept neither an `rsk_` key nor a `mak_` admin key, so there is no scripted equivalent. What automation *can* do: manage keys and limits with an [admin key](/docs/getting-started/api-keys), and read usage, spend trend, and audit logs with an `rsk_` key through the [Usage & Monitoring API](/docs/reference/usage-api) and `/v1/audit-logs`. ### The `/current` model Every org endpoint is scoped to `/current` — the org the caller is signed in to. There is **no `{org_id}` path parameter anywhere in the API**: | Operation | Endpoint | | - | - | | Create an org | `POST /v1/orgs` | | List your memberships | `GET /v1/orgs` | | Get your org | `GET /v1/orgs/current` | | Invite a member | `POST /v1/orgs/current/members` | | List members | `GET /v1/orgs/current/members` | | List pending invitations | `GET /v1/orgs/current/invitations` | | Resend an invitation | `POST /v1/orgs/current/invitations/{inv_id}/resend` | | Create a team | `POST /v1/orgs/current/teams` | | List teams | `GET /v1/orgs/current/teams` | | Team members | `GET`/`POST /v1/orgs/current/teams/{team_id}/members` | | Org usage | `GET /v1/orgs/current/usage` | | Spend by member | `GET /v1/orgs/current/spend` | | Spend by team | `GET /v1/orgs/current/spend/by-team` | | Custom roles | `GET`/`POST /v1/orgs/current/roles`, `PATCH`/`DELETE /v1/orgs/current/roles/{role_id}` | | Transfer ownership | `POST /v1/orgs/current/transfer-ownership` | | Leave the org | `POST /v1/orgs/current/leave` | Because the target org comes from the **session**, acting on a stale one silently hits the wrong org rather than erroring. If you belong to more than one org, switch orgs in the dashboard before making changes. ### Inviting people Invite from **Dashboard → Settings → People**. Creating a member *is* creating an invitation: `POST /v1/orgs/current/members`, what that screen calls, returns an invitation record, and the person joins when they accept it. The invitee accepts through `/v1/invitations` — they can validate a token, list invitations addressed to them, and accept one. You cannot assign `owner` through an invitation, and you cannot change someone into an owner with a role update — ownership moves only through `POST /v1/orgs/current/transfer-ownership`. Attempting either returns `422`. *** ## Shared billing **Every member of an org spends from one balance — the owner's.** There is no per-member wallet. A single member with an uncapped key can consume the entire organization's credit. Per-member spend caps are the control for this, and they are **unlimited by default**. Set them deliberately; see [Rate Limits & Spend Caps](/docs/getting-started/rate-limits). **Settings → Usage** (`GET /v1/orgs/current/spend`, and `/spend/by-team` per team) breaks spend down by member and team, which is the fastest way to find out where the balance went. ## Pooled limits Limits set at org, team, and member level combine with the key's own limits, and **the smallest applicable value wins**. Raising a key's limit does nothing if the team above it is lower. `GET /v1/keys/{id}/limits` shows the resolved result and which tier is binding — see [Rate Limits & Spend Caps](/docs/getting-started/rate-limits). ## Org-wide settings Owners and admins set org-wide policy under **Settings**. Each setting reaches keys differently: * **Model access** — an org-level `allowed_models` / `blocked_models` policy. A model must pass the org, team, member, and key policies together, and a block at any of them wins. * **Data protection** — the org's default PII redaction policy. A key with its own `pii_policy` uses that instead, so `{"enabled": false}` on a key turns redaction off for that key. * **Request bodies** — the org's default for keeping request and response bodies readable. A key's own `body_log_policy` replaces it. * **Data retention** — zero data retention for the whole org. No key can override it, and turning on a key's `body_log_policy` returns `422` while it is on. * **API contract** — the org's `default_api_version`, copied onto keys created after you set it. Existing keys keep their version. See [API Versioning](/docs/reference/api-versioning). ## Audit log Every governance action in your org — key created, member invited, role changed, limit updated — is recorded in an append-only audit trail. ```bash theme={null} curl "https://api.meshapi.ai/v1/audit-logs?limit=50" \ -H "Authorization: Bearer rsk_YOUR_KEY" ``` Audit reads are owner/admin-only, and an `rsk_` key inherits the role of whoever owns it. | Parameter | Notes | | - | - | | `limit` | Page size | | `cursor` | Pagination cursor from the previous page | | `action` | Filter by action type | | `target_type` / `target_id` | Filter by what was acted on | | `actor_id` | Filter by who acted | | `q` | Free-text search | | `from` / `to` | Time window | `GET /v1/audit-logs.csv` returns the same filtered trail as a CSV download for compliance reviews. ## Related * [API Keys](/docs/getting-started/api-keys) — attaching keys to an org or team * [Rate Limits & Spend Caps](/docs/getting-started/rate-limits) — how pooled limits resolve * [Usage & Monitoring API](/docs/reference/usage-api) — org-level usage and spend # Pricing Source: https://developers.meshapi.ai/docs/getting-started/pricing Understand how usage is measured and charged. Mesh API uses a **pre-paid credit** model. You load funds into your account balance and we deduct fractions of a cent per token processed. There are no subscriptions or monthly minimums. ## How token pricing works Usage is billed per token. Pricing is published per **1 million tokens** (the industry-standard "per-1M" convention) and has two components: | Component | Field on `GET /v1/models` | Description | | - | - | - | | **Prompt tokens** | `prompt_usd_per_1m` | Tokens in the messages you send *to* the model | | **Completion tokens** | `completion_usd_per_1m` | Tokens the model generates *back* to you | Completion tokens are typically priced higher than prompt tokens. Different models have vastly different price points — a lightweight model may cost 50× less than a frontier reasoning model. Token-priced models also publish per-1M rates for caching, batch, and other modalities where the provider supports them. ## Non-token pricing units Not every model bills by tokens. Speech, image, and video models publish their rate in the unit they bill in. Check the `pricing` object's `pricing_unit` field (`per_second`, `per_minute`, `per_hour`, `per_1k_chars`, `per_image`, `per_megapixel`, or `per_video`). For these models `prompt_usd_per_1m` and `completion_usd_per_1m` are `null` (a per-token figure would be meaningless), and the real rate is published in: | Field | Meaning | | - | - | | `input_usd_per_unit` | USD per one `pricing_unit` of input — e.g. `0.002` with `pricing_unit: per_hour` is \$0.002 per hour of audio | | `output_usd_per_unit` | USD per one `pricing_unit` of output — e.g. `0.14` with `pricing_unit: per_video` is \$0.14 per video | For token-priced models (`pricing_unit: per_1m_tokens`) the per-unit fields simply mirror the per-1M values. ## Flat per-call fees A few capabilities are billed per call rather than per token. [Web Search](/docs/capabilities/web-search) costs **\$0.005 to \$0.016 per successful search**, depending on the engine that answers, plus \$0.001 per returned page when you ask for page content. Failed or empty searches are free. Budget these by call count, not token volume. ## Free vs. paid models **Free models** (`is_free: true`) cost \$0 for both prompt and completion tokens. They are rare: the moderation model `openai/omni-moderation-latest` is currently the only one, so plan on a paid model for testing too. **Paid models** charge variable rates based on the model's computational cost. Before using paid models, you need a positive credit balance. You can list free models via the Dashboard → **Models** tab or with `GET /v1/models?free=true`. ## Pre-paid balance 1. Add credits in the **Billing** section of the [Dashboard](https://app.meshapi.ai) 2. Credits are debited in real time as inference completes 3. When your balance hits zero, paid model requests return `HTTP 402 Payment Required` with error code `spend_limit_exceeded` 4. Free models continue working regardless of balance, with a tighter limit of their own (20 RPM / 200 RPD per key) Billing settles *after* a response is produced, because the true token count isn't known until then. That means your balance can dip slightly below zero on the last request — it settles on your next top-up. ### Auto-recharge Turn on auto-recharge in **Dashboard → Billing** so a long job never dies mid-run with a `402`. It runs on a UPI AutoPay mandate charged in INR, so it is available only for UPI payments. Each recharge can be \$5–\$100, and you can have one active auto-recharge setup per account. It is managed in the dashboard only; there is no API-key equivalent. ## Spend caps Every API key can have an independent **spend cap** (a maximum USD amount). Once a key's spend reaches the cap, further paid requests from that key are blocked until you raise or reset the cap. This is your primary protection against runaway costs — always set a cap on every key. ## Checking your balance `GET /v1/balance` is read with your `rsk_` API key. ```bash theme={null} curl https://api.meshapi.ai/v1/balance \ -H "Authorization: Bearer rsk_YOUR_KEY" ``` Response: ```json theme={null} { "balance_usd": "42.5000000", "reserved_usd": "15.0000000", "available_usd": "27.5000000", "reserved_breakdown": [ { "kind": "realtime", "reserved_usd": "5.0000000" }, { "kind": "video", "reserved_usd": "10.0000000" } ], "updated_at": "2026-07-14T10:15:30+00:00", "message": null } ``` Money is always a decimal string, never a float. `reserved_usd` is the portion held by in-flight operations — realtime sessions, background jobs, video generation — and `available_usd` is `max(0, balance_usd − reserved_usd)`, the amount you can actually spend. `reserved_breakdown` lists only the categories currently holding funds and sums to `reserved_usd`. `message` is non-null only when an error condition is reported. With an org context the **org owner's** balance is returned — all members draw from one billing pool. See the [Usage & Monitoring API](/docs/reference/usage-api) for the full monitoring surface. # Quickstart Source: https://developers.meshapi.ai/docs/getting-started/quickstart Get up and running with Mesh API in under 5 minutes. Go to the [Mesh Dashboard](https://app.meshapi.ai) and navigate to **API Keys**. Create a new key and copy the `rsk_...` token. This key is shown only once. Store it securely — treat it like a database password. Mesh uses a pre-paid credit model. Add a balance (any amount from ₹1 or \$1) in the **Billing** section to enable inference. Almost every model is paid, and a request on an empty balance is refused with `402`. Use your preferred HTTP client or OpenAI-compatible SDK: ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer rsk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o", "messages": [ {"role": "user", "content": "Explain quantum computing in one sentence."} ] }' ``` ```python theme={null} from openai import OpenAI client = OpenAI( api_key="rsk_YOUR_KEY", base_url="https://api.meshapi.ai/v1", ) response = client.chat.completions.create( model="openai/gpt-4o", messages=[{"role": "user", "content": "Explain quantum computing in one sentence."}], ) print(response.choices[0].message.content) ``` ```typescript theme={null} import OpenAI from "openai"; const client = new OpenAI({ apiKey: "rsk_YOUR_KEY", baseURL: "https://api.meshapi.ai/v1", }); const response = await client.chat.completions.create({ model: "openai/gpt-4o", messages: [{ role: "user", content: "Explain quantum computing in one sentence." }], }); console.log(response.choices[0].message.content); ``` See all available models and their current pricing: ```bash theme={null} curl https://api.meshapi.ai/v1/models \ -H "Authorization: Bearer rsk_YOUR_KEY" ``` Models follow the format `brand/model-name`, where the brand is the organization that trained the model, not the provider that serves it. Examples: `openai/gpt-4o`, `anthropic/claude-sonnet-4.5`, `google/gemini-3.5-flash`. ## What's next? * [Authentication](/docs/getting-started/authentication) — key types, spend caps, and security best practices * [Auto Routing](/docs/capabilities/auto-routing) — let the gateway pick the best model automatically * [Prompt Templates](/docs/capabilities/prompt-templates) — manage prompts server-side * [Dashboard](/docs/getting-started/dashboard) — monitor usage, billing, and logs # Rate Limits & Spend Caps Source: https://developers.meshapi.ai/docs/getting-started/rate-limits How request, token, and spend limits are set across keys, teams, and orgs — and how MeshAPI resolves them when several apply at once. MeshAPI enforces four kinds of ceiling on every request: | Limit | Unit | Enforced per | | - | - | - | | **RPM** | Requests per minute | Fixed window — the clock minute (UTC) | | **RPD** | Requests per day | Fixed window — the UTC day, resetting at midnight UTC | | **TPM** | Tokens per minute | Fixed window — the clock minute (UTC) | | **Spend cap** | USD | Cumulative — lifetime at the key scope, rolling 30 days at every scope above it | Exceeding a rate limit returns `429`. Exceeding a spend cap or running out of credit returns `402`. *** ## Defaults A key with nothing configured still has limits: | Limit | Default when unset | | - | - | | RPM | 100 | | RPD | 10,000 | | TPM | **None — not throttled** | | Spend cap | **None — unlimited** | The spend cap is the one default that is *not* protective. An unset spend cap means **unlimited spend** against your balance, not zero. If you want a key to be unable to burn your whole balance, you must set `spend_cap_usd` explicitly. A key with no `tpm_limit` is not throttled on tokens. `GET /v1/usage/rate-limits` still shows a TPM limit for it: a display value with `is_default_limit: true`, not an enforced one. How high you can set a key yourself depends on your organization's rate-limit tier: | Tier | Max RPM | Max RPD | | - | - | - | | Standard (default) | 1,000 | 1,000,000 | | Enterprise | 20,000 | 5,000,000 | Requests to set a key above your tier's ceiling are rejected with `422`. Limits above your tier are granted by MeshAPI support, up to a platform ceiling of 100,000 RPM and 10,000,000 RPD. *** ## Where limits can be set Limits exist at five scopes: 1. **Key** — `rpm_limit`, `rpd_limit`, `tpm_limit`, `spend_cap_usd` 2. **Team member** — that person's ceiling within one team 3. **Org member** — that person's ceiling across the org 4. **Team** 5. **Organization** All of them default to unlimited except the key-level defaults above. A key-scope spend cap counts **every request the key has ever made**. The scopes above it count only the **last 30 days**, so spend there ages out on a rolling basis and there is no monthly reset date. ### Which person a member limit applies to A member limit is attributed to **whoever created the key**, not whoever sends the request. An inference request carries a key and nothing else, so the API cannot tell two people apart. The key is the identity. If several people share one key, all of their usage counts against the creator's member limit, and the other people's own limits do nothing at all. When that one limit runs out, the key returns `402` for everyone holding it. **Issue one key per person if you want per-person limits to mean anything.** Member limits cannot restrict a shared key per human — they only ever restrict the person who created it. A key also keeps counting against its creator's limit after that person leaves your organization, until someone revokes the key. ### Minimum wins When more than one scope applies, **the smallest value wins** — limits restrict, they never raise. A key with 500 RPM inside a team capped at 100 RPM is effectively limited to 100. This means raising a key's limit has no effect if a tier above it is lower. To actually widen a limit you must raise every tier that binds. ### Checking the resolved value Rather than working it out by hand, ask the API: ```bash theme={null} curl https://api.meshapi.ai/v1/keys//limits \ -H "Authorization: Bearer $MESH_ADMIN_KEY" ``` The response returns both the raw key-level values and the `effective_*` values after the minimum-wins resolution, along with the org, team, and member context that produced them — so you can see *which* tier is binding. This endpoint takes an **admin key** (`mak_...`), not an `rsk_` key. Key management and inference are separate credentials. Create one in **Dashboard → Settings → Admin keys**; see [API Keys](/docs/getting-started/api-keys). The same resolved limits are shown on the key's page in the dashboard. *** ## Request body size Every endpoint has a hard ceiling of **32 MiB (33,554,432 bytes)** on the total request body, independent of your key, team, or org configuration. It is not raisable. An oversized request is rejected **at the edge, before it reaches MeshAPI**. That has consequences worth knowing, because it does not look like the other errors on this page: * the response is a plain HTML `413 Request Entity Too Large` page, **not** a Mesh JSON error envelope — SDKs and error parsers will fail to decode it * there is **no `request_id`**, and the request does not appear in your usage or error logs * it is **not billed**, and it does not consume RPM/RPD The usual cause is Base64-inlined media — images in `/v1/chat/completions` or `/v1/responses`, reference images in `/v1/images/generations`, audio or video inputs. Base64 inflates a payload by roughly **33%**, so a single **\~24 MB** file already exceeds the limit once encoded. To stay under it: * send a **public URL** instead of Base64 wherever the endpoint accepts one * for large files, upload via the [Files API](/docs/capabilities/rag) and reference the returned file ID * downscale or re-compress images before encoding — vision models gain nothing from a 20 MB source image *** ## Watching your usage against limits `GET /v1/usage/rate-limits` reports current consumption against your limits — see the [Usage & Monitoring API](/docs/reference/usage-api). Inference responses also carry your key's request-rate state in OpenAI-style headers, so a client can slow down before it hits a `429`: | Header | Meaning | | - | - | | `x-ratelimit-limit-requests` | The key's own `rpm` limit, or the default RPM if the key sets none | | `x-ratelimit-remaining-requests` | Requests left in the key's own bucket this minute | | `x-ratelimit-reset-requests` | Seconds until the minute window resets | They report RPM only; there are no token headers. They also reflect the key alone: org, team, and member caps are not counted, so a `429` can arrive while `remaining` is above zero. A `429` carries a `Retry-After` that is the time to the window reset plus up to 5 seconds of random jitter, so clients rejected together do not all retry at the same instant. *** ## Behaviours worth knowing **A spend cap can be exceeded by one request.** Billing settles *after* a response is produced, because the true token count isn't known until then. The final request that crosses the cap completes and is charged; the next one is rejected. For the same reason your balance can go slightly negative. Treat the cap as "stop shortly after this", not a hard transactional ceiling. **Free models still consume rate limits.** A model priced at zero costs nothing against your balance or spend cap, but it consumes RPM and RPD like any other request. Free-model requests also have a tighter limit of their own: 20 RPM and 200 RPD per key, counted across all free models together. **Memory injection is checked twice.** Attaching a memory grows the prompt, so the spend cap is re-evaluated against the assembled request. A key near its cap can be rejected once memory is attached even though the raw request would have passed. See [Memory](/docs/capabilities/memory). **`allowed_models` interacts with limits in a surprising way.** Restricting a key to a model list is an access control, not a limit — but it silently breaks features that route to models outside the list. See [API Keys](/docs/getting-started/api-keys). *** ## Related * [API Keys](/docs/getting-started/api-keys) — setting limits and caps on a key * [Usage & Monitoring API](/docs/reference/usage-api) — reading current consumption * [Account Configuration Checklist](/docs/getting-started/account-checklist) — the setup pass over every one of these settings # Install Mesh Code Source: https://developers.meshapi.ai/docs/mesh-code/install Install the Mesh Code desktop app on macOS, Windows, or Linux and connect your API key. Mesh Code is a desktop app for agentic coding on Mesh API. Point it at a project folder and it plans, writes files, runs commands, and starts dev servers there. Every step is either approved by a permission mode you picked or shown to you first, as a real diff or command. Mesh Code needs no Python, Node, or Mesh CLI installed. The app carries its own runtimes. Mesh Code is in **beta**. Prefer a terminal? The [Mesh CLI](/docs/cli/install) runs the same agent from Terminal or PowerShell. ## Install Each installer downloads the current release and checks it against the release's published `SHA256SUMS`. A mismatch stops the install. If the release has no `SHA256SUMS`, no line for your file, or the checksum can't be computed, the installer warns and installs without checking. Requires an Apple Silicon Mac (M1 or later) on macOS 11 or later. There is no Intel build. 1. Press **⌘ + Space**, type `Terminal`, press **Enter**. 2. Run: ```bash theme={null} curl -fsSL https://code.meshapi.ai/install.sh | sh ``` The app is installed to `/Applications` (or `~/Applications` if `/Applications` is not writable) and opens when the install finishes. Requires 64-bit Windows. 1. Click **Start**, type `PowerShell`, press **Enter**. 2. Run: ```powershell theme={null} powershell -ExecutionPolicy ByPass -c "irm https://code.meshapi.ai/install.ps1 | iex" ``` The script downloads `Mesh-Code-Setup-x64.exe` and runs it silently. Launch **Mesh Code** when it prints `Installed.` Requires x86\_64. Mesh Code ships as an AppImage. ```bash theme={null} curl -fsSL https://code.meshapi.ai/install.sh | sh ``` The app is installed to `~/.local/bin/mesh-code`, with a desktop entry so it appears in your app launcher. If `~/.local/bin` is not on your `PATH`, run `~/.local/bin/mesh-code` directly or add the directory to your `PATH`. AppImages need libfuse2 to launch. On Ubuntu 24.04 and later the package is `libfuse2t64`: ```bash theme={null} sudo apt install libfuse2t64 ``` On older Ubuntu and on Debian, install `libfuse2`. Without it, start the app with `~/.local/bin/mesh-code --appimage-extract-and-run`. Prefer to read a script before you pipe it to a shell? Both installers are short: [`install.sh`](https://code.meshapi.ai/install.sh) and [`install.ps1`](https://code.meshapi.ai/install.ps1). ## Direct download Every build is also attached to [GitHub Releases](https://github.com/aifiesta/mesh-code/releases): | Platform | File | | - | - | | macOS (Apple Silicon) | `Mesh-Code-arm64.dmg` | | Windows (x64) | `Mesh-Code-Setup-x64.exe` | | Linux (x86\_64) | `Mesh-Code-x86_64.AppImage` | Each release also carries a `SHA256SUMS` file. Check your download against the line for your file before you run it: ```bash theme={null} shasum -a 256 ~/Downloads/Mesh-Code-arm64.dmg # macOS sha256sum ~/Downloads/Mesh-Code-x86_64.AppImage # Linux ``` ```powershell theme={null} (Get-FileHash $HOME\Downloads\Mesh-Code-Setup-x64.exe).Hash.ToLower() # Windows ``` If the hashes differ, delete the file and download it again. The macOS build is not notarised yet. A `.dmg` downloaded in a browser (or received over AirDrop, Slack, or Messages) is quarantined, and macOS reports the app as *"damaged and can't be opened"*. Check the `.dmg` against `SHA256SUMS` as above. Only if it matches, copy the app to Applications and clear the flag once in Terminal: ```bash theme={null} xattr -dr com.apple.quarantine "/Applications/Mesh Code.app" ``` The `curl` installer does not need this. The Windows installer is not code-signed yet. Run from a browser download, SmartScreen shows *"Windows protected your PC"*. After the checksum matches, click **More info**, then **Run anyway**. The PowerShell installer does not trigger this. ## First launch 1. Get an API key at [app.meshapi.ai](https://app.meshapi.ai) → **API Keys → Create key**. It starts with `rsk_`. See [Authentication](/docs/getting-started/authentication) for key types. 2. Open Mesh Code and paste the key into the **API key** field, then click **Connect**. The app checks the key against Mesh API before saving it. The key is stored in `~/.mesh-code/credentials`, readable only by you (`0600` on macOS and Linux), and is only sent to the Mesh API gateway. The app shows only a hint of it (the last four characters). If you already use the [Mesh CLI](/docs/cli/install) and are signed in, Mesh Code picks up the CLI's key automatically and skips this step. It reads the CLI's key and never changes it. For a guided tour, click **Take the tour** in the sidebar or **Guided tour** in the account menu. It never starts on its own. ## Upgrade Re-run the install command for your OS. It installs the current release over the existing app. ## Data and uninstall Mesh Code keeps its data in `~/.mesh-code` (`%USERPROFILE%\.mesh-code` on Windows): * `credentials`: your API key. * `sessions/` and `transcripts/`: saved conversations, including file contents and command output the agent read. * `journal/`: the project journals. * `context/`: per-project file maps and the agent's saved notes. **Send feedback** in the sidebar posts your rating and comments to a Google Form. To uninstall: * macOS: delete **Mesh Code** from Applications. * Windows: **Settings › Apps › Installed apps › Mesh Code › Uninstall**. * Linux: `rm ~/.local/bin/mesh-code ~/.local/share/applications/mesh-code.desktop` Uninstalling leaves `~/.mesh-code` in place, API key included. Delete that folder to remove your key and history. A key saved by the Mesh CLI stays in `~/.meshapi`. ## Next step Head to [Using Mesh Code](/docs/mesh-code/usage) to open a project and run your first task. Hit a snag? See [Mesh Code troubleshooting](/debug/mesh-code). # Using Mesh Code Source: https://developers.meshapi.ai/docs/mesh-code/usage Open a project, choose what the agent may do, pick models, and track cost in Mesh Code. Once Mesh Code is [installed](/docs/mesh-code/install) and connected to your API key, everything happens inside a project folder you choose. Shortcuts below use **⌘** on macOS; use **Ctrl** on Windows and Linux. ## Open a project Choose **Open Project…** from the menu (**⌘ + O**) and pick a folder. The agent reads and writes inside that folder and runs commands there. * Several folders can be open at once; one is **active**. Each has its own chat, plan, and dev servers, and keeps running when you switch to another. * The file explorer creates, renames, and deletes files (deletes ask first). Click a file to open it beside the chat. * Type `@` in the composer to reference a file, or drag one from the tree onto the message box. * Images can be pasted, dropped, or attached into the composer. Describe the outcome you want, for example *"add a health endpoint and run the tests"*. **Enter** sends, **Shift + Enter** adds a line. Typing while the agent works queues your message for the next turn. **Stop** ends the current turn, including a running command. For multi-step work the agent writes a plan and updates each step's status as it goes, in the transcript and in the **Plan** panel. ## Permission modes The mode picker sits next to the composer and is always visible: | Mode | What runs without asking | | - | - | | **Ask every time** (default) | Nothing. Every file read, file write, command, and web search asks first. | | **Accept edits** | File writes inside the project. | | **Auto** | File writes inside the project, shell commands, and web search. | | **Bypass** | Everything, including file reads and dev servers, except the safety checks below. | Reading a file and starting a dev server ask in every mode except Bypass. When the agent asks, the approval shows the exact diff or command. Choose **Approve**, **Always allow**, or **Reject**. **Always allow** approves that tool for the rest of the session, with the Auto safety checks still applied. An approval left unanswered for 15 minutes counts as a rejection. ### Safety checks Some actions come back to you as a question even when your mode would approve them, with the reason stated: * In Accept edits and Auto: file writes outside the project. In Auto, also shell commands that redirect output, or run `cp`, `mv`, `mkdir` and similar, onto an absolute or `~` path outside the project. * In every mode, including Bypass: file reads and writes under sensitive paths such as `~/.ssh`, `~/.aws`, `/etc`, and `~/.mesh-code`, or with a key extension such as `.pem`. Shell commands that match known dangerous patterns, such as `rm -r`, `rm` aimed at `/` or `~`, `sudo`, and `curl … | sh`. These checks are a heuristic, not a sandbox. They read the path given to the file tools and the text of the command. Anything they do not recognize runs under your mode: `cp ~/.ssh/id_rsa .`, a Python one-liner that opens a file, or a script the agent wrote and then runs. In Auto and Bypass, treat every shell command as able to read or delete anything your user account can. On Windows the checks are written for macOS and Linux paths and commands. The file checks don't cover paths inside `~/.ssh`, `~/.aws` or `~/.mesh-code`, whichever slash you use; only a few key-file extensions are caught. Destructive commands such as `del /s`, `rd /s` and `Remove-Item -Recurse` aren't caught either. Use **Ask every time** or **Accept edits** there if that matters to you. ## Choose a model The model control in the composer bar names whatever decides the model. Its popover holds the routing switch and the effort dial. * **Pinned**: one model for every prompt. Pick it from the model browser: fuzzy search, context window, and per-1M-token pricing for every model in the live catalog, with filters for free and text-only models. * **Smart (local)**: a table bundled with the app classifies each prompt and picks a model per turn. It runs on your machine, bills no extra tokens, and shows which model it would pick and why before you send. * **Gateway auto**: Mesh API's [Auto Routing](/docs/capabilities/auto-routing) picks the model. This costs a classifier hop. With routing on, the bar reads *Smart routing* or *Auto routing* until a step has answered, then shows the model that ran. A **ROUTED** tag marks it when that model differs from your pinned one. In Settings (**⌘ + ,**) you can also set: * **Fallback order**: if the primary model fails or is rate-limited, the gateway tries these models in order. See [Retry & fallback](/docs/platform/retry-and-fallback). * **Never use**: models the router will never pick. * Reasoning effort, writing style, token savings, and **Max steps per turn** (`0` means unlimited). **Settings › Model → Refresh** fetches the latest models and prices from Mesh API. ## Cost The composer bar shows session tokens and cost, updated after each step while the agent works. Each turn also gets a line in the transcript with the model, steps, tokens, cost, and time. Cost is the figure Mesh API billed for each request. When that figure cannot be fetched, the app estimates from the catalog's per-token rates and marks the number with `~`. Session and lifetime totals in the **Account** panel are what this app has spent on this machine, not your account balance. Your authoritative usage and balance are in the [dashboard](https://app.meshapi.ai). ## Panels The right rail holds the panels. It can be resized or hidden, and the sidebar collapses with **⌘ + B**. | Panel | What it shows | | - | - | | **Plan** | The live step list for the current task. | | **Preview** | Dev servers the agent starts, rendered in the app. Stop one from the **RUNNING** list. | | **Shell** | A persistent shell that you type into. History, **Ctrl + C**, and `cd` work on macOS and Linux. It is not gated, because it is you typing. | | **Journal** | The project journal (see below). | | **Account** | Key hint and where the key came from, models available, session and lifetime spend, and **Replace API key**. | ## Command palette and settings * **⌘ + K** opens the palette. Every setting is listed as a runnable action, and you can search by name or by what it does (typing "cheap" finds token savings). Typing `/` in an empty composer opens it too. * **⌘ + ,** opens Settings, grouped by category. * Theme: light, dark, or follow the system. ## Sessions and the project journal * Conversations are saved to disk and restored on launch, with the transcript, plan, and cost totals. * Each project keeps a list of past conversations you can switch between. Sessions are grouped by day in the sidebar, and a dot marks a running one. * The **project journal** is a notebook the agent keeps per project: commands that ran the project, what is built, and learnings. It is added to every new session, so the agent starts out knowing how to run the project. It is stored outside your repo, so it never shows up in `git status`. * Sessions, transcripts, and journals live in `~/.mesh-code`. See [Data and uninstall](/docs/mesh-code/install#data-and-uninstall). ## Current limits * One agent turn at a time per session. Messages you type meanwhile queue. * The Shell panel is a pipe, not a terminal, so full-screen programs such as `vim` or `top` do not run. * On Windows the Shell panel needs a POSIX `sh` on your `PATH` (for example the `bin` folder of Git for Windows), and **Ctrl + C** does not stop a command. Use your own terminal instead. * Web search goes through Mesh API's tools. There is no local browsing. * A model the gateway prices at zero and leaves out of the catalog shows `$0`. Found a rough edge? **Send feedback** in the sidebar opens a short form. For problems, see [Mesh Code troubleshooting](/debug/mesh-code). # Apigee Source: https://developers.meshapi.ai/docs/platform/apigee Route Mesh API traffic through your own Apigee proxy — the target settings you must change, what Apigee cannot carry, and how to verify the path end to end. If your organisation standardises ingress on **Google Apigee**, you can put a pass-through proxy in front of Mesh API and keep a single point where authentication, quotas, analytics and traffic policy are applied. Mesh API needs no change to support this, and neither does your client code — a correctly configured proxy is transparent, and every endpoint behaves identically through it and direct. "Correctly configured" is the whole job. Several Apigee target defaults break Mesh traffic — streaming responses get buffered, slow generation calls time out, and Mesh's own error bodies get replaced by Apigee fault envelopes. This page covers the settings that matter, the traffic Apigee cannot carry at all, and how to prove the path works. Mesh's own docs use **gateway** for the Mesh router itself. On this page, **the proxy** always means your Apigee proxy, and **Mesh API** means the backend behind it. ## How the pieces fit Client traffic terminates at Apigee, which forwards to Mesh API and returns the response unchanged. The proxy does not rewrite paths, alter request or response bodies, or inject its own authentication. ```mermaid theme={null} flowchart LR C[Your client] --> A[Apigee proxy
/meshapi] A --> M[Mesh API
api.meshapi.ai] A -.-> P[auth · quotas · rate limits
analytics · logging] ``` Use a **single catch-all route**. The path suffix after your base path is forwarded verbatim: ``` https://your-apigee-host/meshapi/v1/chat/completions └─────────┬────────┘ │ forwarded as-is ▼ https://api.meshapi.ai/v1/chat/completions ``` Because routing is one catch-all rule rather than a route per endpoint, **new Mesh API endpoints work through your proxy with no proxy change**. Given how often the catalog gains capabilities, per-endpoint routing is a standing maintenance cost with no benefit. ## Base URL Migrating an existing integration is a base-URL change only. Paths, methods, headers, request bodies and response shapes are all unchanged. ```diff theme={null} - https://api.meshapi.ai/v1/chat/completions + https://your-apigee-host/meshapi/v1/chat/completions ``` This works with the Mesh SDKs too — set `base_url` to your proxy and everything else stays as documented in the [SDK reference](/sdk/overview). ## Required target settings These are the settings that differ from Apigee's defaults. Each one is required, and the last three all cause failures that look like Mesh problems but are not. | Setting | Value | Why | | - | - | - | | `request.streaming.enabled` | `true` | SSE on chat completions; multipart audio uploads | | `response.streaming.enabled` | `true` | SSE responses; raw audio from `/v1/audio/speech` | | `io.timeout.millis` | `180000` | The \~55 s default is shorter than image and video generation | | `connect.timeout.millis` | `10000` | | | `success.codes` | `1xx,2xx,3xx,4xx,5xx` | Relay Mesh's real `401`/`404` instead of wrapping them | | Base path | `/meshapi` | Any value; must match your client base URL | | Route | single catch-all | New endpoints need no proxy change | Without the two streaming properties, Apigee buffers the response — SSE arrives in one lump after the generation finishes instead of token by token, and binary audio from `/v1/audio/speech` breaks outright. Without `success.codes`, error semantics change at the proxy: your callers receive Apigee fault envelopes rather than [Mesh's error bodies](/docs/reference/api-overview), so existing error handling stops matching. Start with a bare pass-through and no policies, so that any behaviour change is attributable to the proxy itself. Add quotas, SpikeArrest, caching and API-product keys one at a time afterwards. ## Authentication The proxy passes the `Authorization` header straight through. Use the same Mesh API key you would use directly: ```http theme={null} Authorization: Bearer rsk_... ``` | Condition | Response | | - | - | | No `Authorization` header | `401` with Mesh's own error body | | Invalid key | `401` | | Valid key | `200`, response identical to direct | Errors are relayed from Mesh unmodified — provided `success.codes` is set as above. If you want consumers to hold Apigee-issued credentials instead of a Mesh key, that is an API product and developer app on top of this proxy, with the Mesh key injected at the target. Set up the pass-through first and verify it, then layer that on. ## Verify the path ```bash theme={null} curl --request POST \ --url https://your-apigee-host/meshapi/v1/chat/completions \ --header 'Authorization: Bearer rsk_...' \ --header 'Content-Type: application/json' \ --data '{ "model": "anthropic/claude-haiku-4.5", "messages": [{"role": "user", "content": "Say hi in one sentence."}], "temperature": 0.2 }' ``` **Response:** ```json theme={null} { "id": "chatcmpl-019fdcb94a4d765eb0cd", "object": "chat.completion", "model": "anthropic/claude-haiku-4.5", "choices": [{ "index": 0, "message": {"role": "assistant", "content": "Hi! How can I help you today?"}, "finish_reason": "stop" }], "usage": {"prompt_tokens": 13, "completion_tokens": 12, "total_tokens": 25} } ``` Point the same call at `https://api.meshapi.ai` and compare. A response that differs in anything but `id` and timing means the proxy is transforming traffic it should be passing through. Keep this pair as your regression test. If behaviour changes later, running both isolates the cause at once — a response that differs only through the proxy points at your proxy configuration, and one that matches direct points somewhere else entirely. Re-send with `"stream": true` and confirm chunks arrive progressively rather than all at once at the end. Buffering is the single most common misconfiguration and a non-streaming test will not catch it. ## What Apigee cannot carry ### Realtime audio over WebSocket ``` wss://api.meshapi.ai/v1/realtime wss://api.meshapi.ai/v1/audio/transcriptions/realtime wss://api.meshapi.ai/v1/audio/speech/stream/{voice_id} ``` **Apigee does not proxy WebSocket traffic.** This is a platform limitation, not a configuration issue — there is no proxy setting that enables it. [Realtime](/docs/capabilities/realtime-audio) and streaming-speech clients must connect **directly to Mesh API**, bypassing the proxy. Any network policy that assumes all traffic flows through Apigee needs an explicit exception for these endpoints. ### File uploads bypass the proxy `POST /v1/files` returns a pre-signed Google Cloud Storage URL: ```json theme={null} { "file_id": "019fdcb7-756c-6c46-82fe-fb290d0a6533", "signed_url": "https://storage.googleapis.com/mesh-rag-files-prod/..." } ``` Your client then `PUT`s the bytes **directly to `storage.googleapis.com`**. That transfer does not pass through Apigee, so proxy policies, quotas and analytics apply only to the metadata call that reserves the record — never to file content. Egress rules must allow the storage host. See [Files & RAG](/docs/capabilities/rag) for the full upload flow. ### Payloads above \~10 MB Apigee buffers messages with a default ceiling of roughly **10 MB**. Typical Mesh payloads sit comfortably inside it: | Payload | Size | | - | - | | `GET /v1/models` response | \~5.8 MB | | Image generation response | 1.63 MB | | `GET /v1/audio/voices` response | 276 KB | The unfiltered model list is the closest to the ceiling and grows with the catalog; filter it with `?type=` or `?provider=`, or fetch one model with `GET /v1/models/{model_id}`, to keep responses small. Beyond that, requests may be rejected at the proxy with a `413` that does not occur direct. Large file content should use the pre-signed upload flow above rather than inline payloads. ### Slow calls against the default timeout Generation endpoints routinely exceed Apigee's default target timeout, which is why `io.timeout.millis` is on the required list. Observed durations through a pass-through proxy: | Operation | Time | | - | - | | Image edits | \~33 s | | Image generation | \~28 s | | Chat compare, 3 upstream models | \~8 s | | Web search | \~5 s | | Metadata reads | \< 150 ms | [Video generation](/docs/capabilities/video-generation) is asynchronous — you poll a task, so no single call is long-running. A `504` from the proxy on a call that succeeds direct means this ceiling needs raising. ## Errors that are not proxy-related Three Mesh API responses are easy to mistake for proxy faults during a cutover. Each reproduces identically against the backend direct, so diffing the two calls identifies them straight away. | Response | What it means | | - | - | | `404 model_not_found` | The model ID is not in the catalog. Send the brand-namespaced `id` from `GET /v1/models`, e.g. `anthropic/claude-haiku-4.5`. A bare name such as `claude-haiku-4.5` resolves only when exactly one catalog model has that name. See [Models](/docs/reference/models) | | `422` on usage endpoints | Usage endpoints take `org_id` as a required parameter. Pass it explicitly on every call; see [Usage API](/docs/reference/usage-api) | | `400 Disallowed CORS origin` | Browser origins are allowlisted. A proxy in front of Mesh changes the `Origin` your callers present, so the new origin needs adding | Only the last is affected by the proxy at all. If you serve browser clients through Apigee, allowlist the proxy-facing origin before you cut over. ## Verified coverage Mesh verifies every documented endpoint through a pass-through Apigee proxy and against the backend direct, then diffs the two. **No endpoint behaves differently through the proxy** — every group returns responses matching direct: | Group | Endpoints | Notes | | - | - | - | | Chat | 2 | Includes SSE streaming | | Core inference | 4 | Responses, messages, embeddings, moderations | | Images | 2 | Generation, edits | | Video | 3 | Asynchronous job model | | Audio | 6 HTTP | Excludes the WebSocket endpoint | | Batches | 4 | Create, get, cancel, list | | Files | 5 | Init, status, embed, search, list | | Memories | 12 | Full CRUD including items | | Templates | 5 | Full CRUD | | Models | 3 | | | Usage | 5 | | | Audit logs | 2 | Includes CSV export | | Router | 1 | | | Account | 2 | Balance, error catalog | | Web | 1 | | The realtime audio WebSocket is the sole exclusion, for the reason above. ## Troubleshooting | Symptom | Likely cause | Action | | - | - | - | | TCP connects, TLS handshake dropped | Load balancer certificate not yet `ACTIVE` | `gcloud compute ssl-certificates describe` | | SSE arrives all at once | Response buffering | Confirm `response.streaming.enabled=true` | | `504` at the proxy, works direct | Target timeout too low | Raise `io.timeout.millis` | | `413` at the proxy, works direct | Payload over \~10 MB | Use the pre-signed upload flow | | Apigee fault envelope instead of a Mesh error | `success.codes` not set | Set it to `1xx,2xx,3xx,4xx,5xx` | | WebSocket fails to connect | Apigee cannot proxy WS | Connect direct to Mesh API | | `400 Disallowed CORS origin` | Origin not allowlisted | Add the proxy-facing origin | | A new endpoint 404s at the proxy | Route is not catch-all | Replace per-endpoint routes with one catch-all | Anything that reproduces against `https://api.meshapi.ai` directly is not a proxy issue — see [Troubleshooting](/debug/mesh-api) or contact [support](/docs/reference/support). # Architecture Source: https://developers.meshapi.ai/docs/platform/architecture Behind the scenes of the Mesh AI Router. The Mesh API is more than just a proxy; it's a high-performance orchestration layer that manages dozens of AI providers, monitors costs, and ensures maximum availability. ## High-Level Workflow When you call `/v1/chat/completions`, the Mesh Router performs the following tasks: 1. **Authentication**: Validates your `rsk_` key, checks for active balance, and verifies spend caps. 2. **Provider Selection**: (If multiple providers offer the same model) Identifies the provider with the lowest current latency and cost. 3. **Secret Retrieval**: Fetches your encrypted provider keys from GCP Secret Manager (with memory-caching for sub-millisecond lookups). 4. **Request Transformation**: Converts your request into the provider-specific format (e.g., Anthropic Messages API or Google Vertex AI). 5. **Streaming/Buffering**: Pipes the response back to your client in real-time (supporting Server-Sent Events). 6. **Usage Logging**: Records the total tokens consumed, calculates the final cost in USD, and updates your balance atomically. ## Resilience Features * **Automatic Failover**: If an upstream provider returns a transient error, Mesh retries and then re-sends the request for the same model through another provider that serves it. This is on by default for every key. See [Retry & Fallback](/docs/platform/retry-and-fallback). * **Circuit Breaking**: Mesh monitors error rates for each model-provider pair. If a provider starts failing, it's temporarily removed from the routing pool. * **Regional Optimization**: To minimize latency, Mesh routes requests to the nearest cloud region for global providers (Bedrock, Vertex AI). ## Security & Privacy * **Key Scoping**: API keys are isolated per owner. Your keys can never be used by another account. * **Request logs**: By default, Mesh stores the request and response bodies of each API call in its request logs, next to the metadata (tokens, model, latency, cost). With PII redaction on, the stored request body is the redacted copy. The bodies Mesh sends to upstream providers are kept for 14 days. * **Zero data retention**: An org owner or admin can turn it on under **Settings → Data retention**. Mesh then stores no request or response bodies for any key in the org, including the copies sent upstream, and keeps only the metadata. See [Organizations](/docs/getting-started/organizations#org-wide-settings). * **Encrypted Secrets**: All upstream keys are stored in a hardware-isolated GCP Secret Manager with restricted IAM access. # Retry & Fallback Source: https://developers.meshapi.ai/docs/platform/retry-and-fallback How Mesh API automatically retries and reroutes requests when an upstream provider returns a transient error — which status codes trigger it, and how same-provider retries, cross-provider fallback, and model fallback fit together. Upstream providers occasionally fail for reasons that have nothing to do with your request — a momentary `503`, an overloaded region, a brief rate-limit spike. Mesh API absorbs these transient failures for you: instead of returning the first error it sees, the gateway automatically **retries** and, if needed, **reroutes** your request before responding. This is built into the platform and happens **server-side**. You don't enable it, configure it, or change your code — you send a normal request and Mesh API does the resilience work behind the scenes. ## Two independent layers Alongside the gateway's built-in behaviour, the Mesh API SDKs can retry a failed call themselves. The two operate at different scopes and are safe to combine: | | Gateway routing policy | SDK client-side | | - | - | - | | Where it runs | Inside the gateway, before the response returns | In your process, around each request | | Retry | Same provider/target | Same request to the gateway | | Fallback | **Same model on another provider**, then a **different model** if needed | None | | Configured via | Dashboard, per key or through a routing-policy template | The client's `max_retries` / `maxRetries` / `MaxRetries` option | | Applies to | Every endpoint except `/v1/moderations`; streams only before the first token | Requests the SDK sends | A typical setup: let the **gateway** absorb provider-level blips and reroute around a failing provider, with no code change, and keep the SDK's retry for failures between your process and the gateway. To switch to an entirely different model, list it in the policy's `fallback.models` (see below) or handle the final error in your own code. **`/v1/moderations` is the one endpoint with no gateway routing.** It has its own error handling, but the retry budget, cross-provider fallback, circuit breaker and per-key routing policy described below do not apply to it. If you depend on retries there, handle them in your own client. **Every other endpoint runs this policy.** Chat completions, `/v1/responses`, embeddings, and image, video and audio generation all resolve your key's routing policy, retry, plan cross-provider and cross-model fallback targets, and share the same circuit breaker. One consequence is worth knowing before it appears on an invoice: because cross-**model** fallback applies to these endpoints too, an image, video or embeddings request can be served by a different model than the one you asked for, and is billed at the rate of the model that actually served it. It is not silent — the response carries `X-Mesh-Routing-Model` naming the substitute, and the response body's own `model` field always reports what served the request. ## What happens when a request fails When an upstream returns an error, the gateway responds in escalating steps. It only moves to the next step if the previous one couldn't recover: The gateway waits briefly, then re-sends the request to the same provider. Transient blips often clear on the second or third try. If the provider keeps failing, the gateway re-issues the request for the **exact same model** through a different upstream that serves it. You still get the model you asked for. If the requested model can't be served anywhere right now, the gateway can route to a comparable alternate model so the request still succeeds. All of this runs inside a single request. From your side, you make one call and receive one response — the retries and reroutes are invisible unless you look at the [response headers](#seeing-what-happened) or your dashboard logs. ```mermaid theme={null} flowchart TD A[Request arrives] --> E[Send to primary provider] E --> F{Result?} F -- Success --> S[Return response ✓] F -- "Terminal error
auth · invalid input · billing" --> X1[Return error immediately ✗] F -- "Transient error
408 · 429 · 500 · 502 · 503 · 504" --> H{Retries left
for this provider?} H -- yes --> I[Back off, then retry same provider] I --> E H -- no --> J{Same model available
on another provider?} J -- yes --> K[Retry the same model
on a different provider] K --> F J -- no --> L{A fallback model
available?} L -- yes --> M[Try a different model] M --> F L -- no --> X2[Return the last error ✗] ``` ## Which errors trigger retry and fallback Only **transient** failures — errors that could plausibly succeed on a second attempt — trigger retries and fallback. By default these are the HTTP status codes: | Status | Condition | | - | - | | `408` | Request Timeout | | `429` | Too Many Requests (rate limited) | | `500` | Internal Server Error | | `502` | Bad Gateway | | `503` | Service Unavailable | | `504` | Gateway Timeout | The same set governs both same-provider retries and cross-provider fallback: a provider must return one of these (or time out with no response at all) for the gateway to try again. ### Errors that are never retried Some failures mean something is wrong with the request itself — trying again would only produce the same error. These are **terminal**: the gateway returns them immediately, with no retry and no fallback. | Category | Examples | | - | - | | **Authentication / authorization** | invalid or missing key, insufficient permissions (`401`, `403`) | | **Invalid input** | malformed request, unsupported parameters, prompt over the model's context limit (`400`) | | **Billing** | insufficient credits or a billing block | A `429` (rate limit) is treated as transient and *is* retried, because it typically clears on its own or resolves once the request is routed to another provider. ### Capability mismatches fall over, even though they aren't transient One case sits outside the status table above. When a model simply cannot do what you asked — tool calling inside a stream, vision input, a feature it doesn't implement — the provider reports it as a `400` or `422`, which is not a transient status. Retrying that same model is futile; the answer will never change. But the **next-ranked model may support the feature**. So a capability mismatch is treated as terminal *for that model* while still being eligible for fallover to a different one. This is why a request asking for an unsupported feature can still succeed on a model you didn't name. If no alternate can serve it either, you get a clean error — a capability mismatch is never silently swallowed. ## How retries are paced Retries against the same provider use **exponential backoff with jitter** — the gateway waits a short, randomized delay that grows with each attempt, so a provider recovering from a spike isn't immediately hammered again. A provider is retried only a small number of times before the gateway moves on to fallback. The whole sequence — the initial call, same-provider retries, and every fallback hop — shares **one attempt budget and one wall-clock deadline**. Retries and fallbacks stop once that budget is spent, so a struggling upstream can never make your request hang indefinitely: you get a result, or a final error, within a bounded window. Because the budget is *shared*, the primary target could otherwise consume all of it and leave nothing for a fallback hop. The gateway holds one attempt back while alternates remain, so a failing primary cannot starve the fallback it exists to trigger. The practical consequence: a request is **not** retried indefinitely across every provider that serves your model — it is a budget, not a promise. ## Per-key routing policy Every key runs a **routing policy**. A key with no policy of its own uses, in order: the routing-policy template attached to it, your org's default template, and finally the **System default**. The System default allows up to 2 retries on the same provider, then fallback to the same model on another provider, within 3 attempts and 60 seconds. While another provider is left to try, one of the 3 attempts is held for that fallback, so the first provider gets at most 2. That default is why the behaviour above needs no setup. To change it for one key, edit the key in the [Dashboard](https://app.meshapi.ai) (**API Keys → edit a key → Resilience**). To share one policy across keys, create a template under **Routing Policies** and attach it, or make it your org's default. With an admin key you can list, update, and delete templates at `/v1/routing-policy-templates`; creating one is a dashboard action. The policy applies to every endpoint that runs gateway routing, with no client changes. The policy is a JSON object with three blocks — `retry`, `fallback`, and `budget`: ```json theme={null} { "retry": { "max_retries": 3 }, "fallback": { "enabled": true, "cross_provider_same_model": true, "models": ["anthropic/claude-sonnet-5"] }, "budget": { "max_attempts": 6, "max_total_latency_ms": 180000 } } ``` * **`retry.max_retries`** — how many times the gateway retries the *same* target before falling back. `0` turns same-target retry off. * **`fallback`** — when `enabled` and `cross_provider_same_model` are `true`, the gateway re-issues the request for the **same model against a different provider**. `models` is an ordered list of up to 3 **alternate models**, tried after the same-model hops are exhausted; an entry that is unknown or unpriced is skipped, never failing the request. * **`budget`** — a hard stop across all attempts, so retries and fallbacks can never run unbounded. These are the only fields a policy accepts. Anything else, for example `retry_on_status`, `base_ms`, or `fallback.on_status`, is rejected with `422`; those always follow the system defaults described below. A field you leave out also follows the system default. The same object can carry `weight_profile` and `router_version`, which apply only to `model: "auto"`. See [Auto Routing](/docs/capabilities/auto-routing). ### Ceilings Values above these limits are clamped when the policy is applied: | Field | Ceiling | | - | - | | `retry.max_retries` | ≤ 6 | | `budget.max_attempts` | ≤ 6 | | `budget.max_total_latency_ms` | ≤ 180000 (180 s) | ### What each parameter controls | Decision | Controlled by | | - | - | | Is a policy active at all? | Always. A key without its own policy resolves its attached template, then the org default template, then the System default. | | How many targets are in the plan? | `fallback.enabled` + `fallback.cross_provider_same_model` add the other providers serving the same model; `fallback.models` adds alternate models after them; the plan is capped at `budget.max_attempts` targets. | | Is this target temporarily out of the pool? | The **global** circuit breaker (platform-managed, not per-key) — a skipped target falls through to the next one. | | How long may a single attempt run? | Not configurable. There is no per-attempt bound; the overall `budget.max_total_latency_ms` applies. | | Never-retry safety rule | Not configurable. Auth, validation, and billing errors are terminal on any policy. | | Retry the same target? | All three must hold: the status is transient (the table [above](#which-errors-trigger-retry-and-fallback)), attempts so far are below `retry.max_retries` + 1 (and below `budget.max_attempts`), and the next attempt would still finish inside `budget.max_total_latency_ms`. | | How long is the pause between retries? | Not configurable. Full-jitter exponential backoff: a random wait of up to 200 ms before the first retry, with the ceiling doubling each retry to a 4 s cap. A provider's `Retry-After` is not used. | | Move to another provider? | `fallback.enabled` and `fallback.cross_provider_same_model` must be true and the error transient. The same attempt budget and deadline keep applying. | | Pause before the next provider | None. The next provider is tried immediately. | ### Worked example Policy: `retry.max_retries = 1`, `fallback.enabled` and `cross_provider_same_model` both `true`, `budget.max_attempts = 4`. The primary provider is having an outage: 1. **Attempt 1** — primary provider returns `503`. Transient, budget remains → retry. 2. **Attempt 2** — after a short backoff, primary again returns `503`. `max_retries` (1) is exhausted for this target → check fallback: eligible. 3. **Attempt 3** — second provider, same model → `200 OK`. The response arrives with `X-Mesh-Routing-Attempts: 3` and `X-Mesh-Routing-Fallback: true`, and the dashboard log row shows a **Provider fallback** badge and *Retried ×2*. Had attempt 1 failed with a `401` instead (terminal), it would have failed immediately — attempts 2 and 3 never happen, whatever the policy says. ## Failing providers are taken out of rotation Alongside per-request retries, Mesh API runs a platform-wide **circuit breaker**. When a specific model-and-provider combination starts failing repeatedly, it is temporarily pulled from the routing pool for everyone, then re-checked automatically after a short cooldown. This keeps requests from being routed into an upstream that is already known to be down, so fallback skips straight to a healthy provider. This is a global safety mechanism managed by the platform — it isn't tied to any individual request or key. ## What retry and fallback cost you **Failed attempts are not billed.** Only an attempt that actually returned a response is charged, so the retries and fallback hops described above are free. If the gateway tries a provider three times and then reroutes, you pay once — for the answer you received, not for the attempts it took to get there. This is worth stating plainly because the intuitive assumption is the opposite: resilience does not mean paying for resilience. When the gateway falls back to the **same model on a different provider**, the model that answers is the one you requested, and billing is unchanged. When it falls back to a **different model**, you are billed for the model that actually served the request — which may be more or less than the model you asked for. The response tells you what happened, so the substitution is never silent. ### Charges that surprise people Three cases where a bill is larger than a naive "one request, one charge" reading predicts. None of them is a retry. **Disconnecting mid-stream still bills what was generated.** If your client drops a streaming response after output has started, the tokens produced up to that point are charged. The provider generated them and billed us for them whether or not your application read them, so cancelling a stream is not a way to avoid paying for it. Cancel early if you need to cancel. **`model: "auto"` bills a classifier call.** Automatic model selection runs a small internal model to pick the target, and that call is billed alongside the answer. It is a fraction of the cost of the request it routes, but it is not free — see [Auto Routing](/docs/capabilities/auto-routing). **A different model means a different rate.** Covered above, and repeated here because it is the one that shows up as an unexpected line on an invoice: model fallback protects availability, not price. **Streaming requests** are protected only *before the first token is sent*. Once a stream has started emitting output, an error mid-stream is returned as-is — a partially streamed response can't be retried or rerouted without duplicating output. ## Seeing what happened On a **non-streaming** response, the gateway reports what it did through response headers: | Header | Meaning | | - | - | | `X-Mesh-Routing-Attempts` | Total attempts made (the initial try plus any retries and fallback hops). | | `X-Mesh-Routing-Fallback` | `true` if the request was ultimately served by a fallback rather than the primary provider. | | `X-Mesh-Routing-Model` | Present only when a cross-model fallback served the request: the model that answered. | These headers do not name which upstream served the request — they tell you *that* a retry or fallback happened and how many attempts it took. Your Mesh API dashboard logs show the same information per request, including for streamed calls that don't carry these headers. **Streaming responses do not carry these headers.** Response headers are flushed before the SSE body starts, so any retries happen inside the stream generator after the headers have already been sent. For per-request routing detail on streamed calls, use the dashboard [Logs](https://app.meshapi.ai) — the log row shows the attempt count and a **Provider fallback** badge. ### Not to be confused with `X-BYOK-Fallback-Triggered` The `X-Mesh-Routing-*` headers describe **routing** fallback. That is a different mechanism from [BYOK](/docs/capabilities/byok) **credential** fallback: | Header | Mechanism | What "fallback" means | | - | - | - | | `X-Mesh-Routing-Fallback` | Resilient routing | The request was re-issued to the same model on a **different provider**. | | `X-BYOK-Fallback-Triggered` | [BYOK](/docs/capabilities/byok) | Your own provider key failed, so the request fell back to Mesh API's **shared platform credentials**. | Both can appear on the same response — a request can fall back from your BYOK credentials *and* be routed across providers — but they answer different questions. In the dashboard Logs they render as distinct **BYOK fallback** and **Provider fallback** badges. ### Turning credential fallback off: `allow_fallback` BYOK credential fallback is controlled per credential by **`allow_fallback`**, which is **enabled by default**. When your own provider key fails with an auth or rate-limit error, the gateway retries the request on Mesh API's shared platform credentials so it still succeeds. **Why you might deliberately disable it.** Some organisations must guarantee their traffic only ever touches *their own* provider account — for contractual reasons, data-residency commitments, or because their agreement with the upstream provider governs how that data may be processed. Silently completing a request on Mesh API's shared credentials would break that guarantee, even though it would make the individual request succeed. Set `allow_fallback` to `false` on the credential and a failure of your key is returned to you as an error instead of being rerouted. You are choosing a failed request over an unexpected one — which, under an isolation commitment, is the correct trade. The specific status codes, retry counts, timing, and fallback behaviour described here are the platform's current defaults and may change in the future as we tune reliability. Treat retry and fallback as a best-effort resilience layer rather than a fixed contract, and always handle a final error response in your own code. # API reference Source: https://developers.meshapi.ai/docs/reference/api-overview What the Mesh API covers, its base URL, authentication, and how to use the interactive explorer. The Mesh API provides a unified interface for accessing various AI models through a single API key. This interactive reference allows you to explore the available endpoints, test them directly in the browser, and see code snippets in your preferred language. ## Key Features * **Chat Completions**: OpenAI-compatible text, multimodal, tool-calling, and streaming inference. * **Messages and Responses**: Anthropic-compatible `POST /v1/messages`, and the Responses API for reasoning models. * **Embeddings and RAG**: Dense vectors, plus file upload, embedding, and semantic search over your own documents. * **Images, Audio, and Video**: Image generation and editing, text-to-speech, transcription and translation, and asynchronous video generation. * **Compare and Batches**: Run one prompt across several models, or submit asynchronous batch jobs and read results inline. * **Web search, Moderations, and Evaluate**: Ranked web results, content classification, and typed probability answers from evaluation models. * **Auto Router**: Ask which model best fits a request (`POST /v1/router/select`). * **Model Discovery, Templates, and Memory**: Browse and search the catalog, version and publish prompt templates, and attach per-user memory to requests. * **Account**: Balance, usage and rate-limit reports, audit logs, admin keys, end-user data erasure, and API versions. [Realtime audio](/docs/capabilities/realtime-audio) (WebSocket) and the [MCP server](/agent/mcp) are documented in their own guides, not in this reference. ## Base URL All API requests should be made to: ```bash theme={null} https://api.meshapi.ai ``` ## Authentication The Mesh API uses Bearer token authentication. You can obtain your API key from the Mesh developer dashboard. ```bash theme={null} Authorization: Bearer ``` ## Versioning The `/v1` in every endpoint path is a stable namespace, not a version. Mesh versions its contract by date, in a header. The newest version is `2026-09`: ```bash theme={null} X-Mesh-Version: 2026-09 ``` Send no header and you get your key's pinned version, or the oldest supported version (`2026-08`) if the key has none. A new key copies your organization's default version, and new organizations default to the newest version, so check the key's **API Version** under **API Keys** before you pin it. An unsupported version is rejected with `400 invalid_api_version` rather than silently downgraded. See [API versioning](/docs/reference/api-versioning). ## Interactive Explorer You can use the "Try It" button on any endpoint to send real requests to the Mesh API. Make sure to provide your API key in the authentication section. # API versioning Source: https://developers.meshapi.ai/docs/reference/api-versioning Mesh versions its contract by date. Pin a version with X-Mesh-Version so a future change to a response shape cannot change it underneath your code. Mesh versions its API **by date**, not by a number in the URL. The newest version is `2026-09`; if you send nothing you get `2026-08`. You select one with a header: ```bash theme={null} curl https://api.meshapi.ai/v1/models \ -H "Authorization: Bearer rsk_..." \ -H "X-Mesh-Version: 2026-09" ``` The `/v1` in the path is **not** a version. It is a stable namespace and it is not going to change — `/v2` is not planned. Everything about the shape of a request or response is governed by the dated version instead. ## Why a version header Additive changes — a new field, a new optional parameter, a new endpoint — are shipped continuously and do not need a version. You get them without doing anything. A dated version exists for the changes that are *not* safe: renaming a field, removing one, or changing what a value means. Rather than break your integration or freeze the API forever, those changes land in a new dated version, and requests pinned to an older one keep receiving the older shape. Pinning is what makes that a guarantee rather than a coincidence. A pinned request states which shape your code can parse, so a change to the current shape cannot reach it. ## If you send no header You are served the **baseline** — the *oldest* supported version, which today is `2026-08`. That is a deliberate choice, and worth understanding because it inverts what people usually expect from a default: Publishing a new version never moves existing traffic. Because the baseline is the oldest supported version rather than the newest, an integration that sends no header keeps getting the same shapes after a new version ships. You move only by pinning the new version yourself. So there is no urgency to pin — but until you do, your code has not stated what it can parse, and the guarantee above does not apply to it. ## Which versions are served `GET /v1/api-versions` lists them, oldest first. It takes your `rsk_` API key. ```bash theme={null} curl https://api.meshapi.ai/v1/api-versions \ -H "Authorization: Bearer rsk_..." ``` ```json theme={null} [ { "label": "2026-08", "status": "ga", "released": "2026-08-15", "deprecated_on": null, "sunset_on": null, "notes_url": null, "baseline": true, "latest": false, "capabilities": [] }, { "label": "2026-09", "status": "ga", "released": "2026-09-03", "deprecated_on": null, "sunset_on": null, "notes_url": null, "baseline": false, "latest": true, "capabilities": ["video.poll_by_request_id"] } ] ``` `baseline` marks what an unpinned request gets; `latest` marks the newest. They are now two different entries — `latest` is deliberately **not** the default, so shipping `2026-09` did not move anyone. `capabilities` lists behaviour a version *adds*, and it is cumulative: everything introduced at or before that label. Unlike a change to a response shape, a capability is something you can only use once you pin for it — see [What each version added](#what-each-version-added). Worth wiring into your own tests: assert the version your code pins still appears here, and that its `status` is not `sunset`. That turns "our pin went stale" into a failing test instead of a 400 in production. ## Pinning a whole API key Rather than sending the header on every request, you can pin the key itself — useful when the calling code is not yours to change, or when a whole integration should sit on one version. Set it when creating a key, or on an existing one — key management takes an [admin key](/docs/getting-started/api-keys) (`mak_...`), not the `rsk_` key being pinned: ```bash theme={null} curl -X PATCH https://api.meshapi.ai/v1/keys/{key_id} \ -H "Authorization: Bearer $MESH_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"api_version": "2026-08"}' ``` Send `"api_version": null` to clear it. In the dashboard the same setting is on the key's **API Version** field under **API Keys**. Resolution order, highest first: 1. The `X-Mesh-Version` **header** on the request — always wins. 2. The **key's** stored pin. 3. The **baseline**. A key's pin is resolved when the key is created, from your organisation's default at that moment — so a key created without an explicit version may show a concrete one afterwards rather than staying empty. Changing your org default later does not move existing keys. An unsupported version stored on a key is treated differently from an unsupported header: the request falls back to the baseline rather than failing. A typo in *your* header should be loud; a version we retired out from under a stored pin should not break your traffic. ## Unsupported versions are rejected, not downgraded If you send a version Mesh does not serve, the request fails: ```json theme={null} { "error": { "code": "invalid_api_version", "message": "API version '1999-01' is not supported. Supported versions: 2026-08, 2026-09.", "supported_versions": ["2026-08", "2026-09"] }, "request_id": "req_01ARZ3NDEKTSV4RRFFQ69G5FAV" } ``` `400`, with the versions that *are* served, and no fallback. This is intentional, and it is the reason pinning is trustworthy. If a typo or a stale constant quietly fell back to some other version, you would believe you were pinned when you were not, and find out from a shape mismatch in production instead. A `400` on the first call is the better failure. For the same reason, an **empty** value (`X-Mesh-Version:` with nothing after it) is treated as a malformed pin, not as "no pin". If your client builds the header conditionally, omit it entirely when you have no version to send rather than sending it blank. Validation runs before authentication, so a bad version is rejected with `400` even if the key is also wrong. ## Reading the version that served a request Every response carries `X-Mesh-Version` with the version that served it — including error responses, and including requests that sent no header at all. So you can confirm what you actually got rather than assume it, and you can see which version a stored key pin resolved to without looking the key up. ```bash theme={null} curl -i https://api.meshapi.ai/v1/models -H "Authorization: Bearer rsk_..." # ... # x-mesh-version: 2026-08 ``` It is in the CORS expose-headers allowlist, so browser JavaScript can read it too. ## Format A version label is `YYYY-MM`, e.g. `2026-08`, or `YYYY-MM-DD` for a version cut later in a month, e.g. `2026-08-18`, which is newer than `2026-08`. Anything else, including a well-formed label that names no version, is rejected with the same `400 invalid_api_version`. Pass it as a literal string. Do not compute it from the current date: the label names a contract, not today's month, and there is no version for most months. ## Which version to pin Pin the version you developed and tested against, and change it deliberately. Concretely: hold the label in one constant in your codebase, send it on every request, and treat bumping it as a change with its own review — because a new version exists precisely when some shape is different, so moving to it means reading what changed. Do not read the version from configuration that varies by environment. A staging deployment pinned to one version and production pinned to another means the shape your tests exercise is not the shape production parses, which defeats the point. ## What is not covered by a version * **Model availability and pricing.** Models are added, retired, and repriced continuously. Pin a version and you still get the current catalogue — see [Models](/docs/reference/models). * **Rate limits and quotas.** Governed by your plan, not the contract. See [Rate limits](/docs/getting-started/rate-limits). * **Provider behaviour.** A model's output quality or latency is the provider's, and no version freezes it. * **Bug fixes.** A response that did not match its documented shape gets corrected in place, in every version. ## Realtime The realtime WebSocket API (`/v1/realtime`) negotiates separately and does not read this header. Sending it on the handshake has no effect. ## Knowing something is going away, without reading this page When a version or an endpoint is scheduled for retirement, its responses start carrying standard headers — so your monitoring can find out before a human does: | Header | What it says | | - | - | | `Deprecation` | When it was deprecated. An [RFC 9745](https://www.rfc-editor.org/rfc/rfc9745.html) structured-field date — `@1788220800`, seconds since the epoch | | `Sunset` | When it stops being served. An [RFC 8594](https://www.rfc-editor.org/rfc/rfc8594.html) HTTP-date — `Wed, 01 Sep 2027 00:00:00 GMT` | | `Link` | `rel="deprecation"` pointing at the migration notes, when there are any | **The two dates are in different formats** — a unix timestamp and an HTTP-date. If you parse one and reuse the parser on the other you will silently get nothing. That is a property of the RFCs, not of Mesh. `Sunset` may be absent while `Deprecation` is present: "deprecated, retirement date to be announced" is the normal state at announcement time. Absence means undecided, not imminent. These arrive on **every** response for an affected route, including errors — a `429` from a deprecated endpoint is still deprecated, and an error response is often the only one a client logs. Nothing is deprecated today, so nothing currently emits them. If you read these from browser JavaScript, note they are already in the CORS `Access-Control-Expose-Headers` allowlist along with `X-Mesh-Version`. ## What each version added A video generation task can be fetched with the `request_id` Mesh returned for the request that created it, as well as with the provider task id. Video task bodies also carry a `request_id` field. Capability: `video.poll_by_request_id`. See [Video generation](/docs/capabilities/video-generation#poll-for-the-result). The contract as it stood when dated versions were introduced. This is what you are served if you send no header and have set no pin. ## Support window Every version `GET /v1/api-versions` lists is served. Both `2026-08` and `2026-09` are `ga`, nothing is scheduled for retirement, and no pin can go stale today. A version is never withdrawn without notice. When one is scheduled for retirement it starts returning `Deprecation` and `Sunset` response headers, so your client can detect it without reading this page. If you need the retirement policy's exact numbers before committing to a pin, [contact support](/docs/reference/support). # Error Reference Source: https://developers.meshapi.ai/docs/reference/errors Every error code the API returns, which side of the call it belongs to, and what to do about it. Every failure returns the same envelope. `error.code` is stable and safe to branch on; `message` is for humans and may change. ```json theme={null} { "error": { "code": "context_window_exceeded", "message": "This model's maximum context length is 200000 tokens.", "provider_code": "context_window_exceeded" }, "request_id": "req_01JQ..." } ``` `code` is always present. `provider_code` appears when the failure came from a model provider and carries the more specific reason — branch on `code` first, then narrow on `provider_code` if you need to. Always log `request_id`. It is the only handle that ties your failure to our logs, and support cannot trace a report without it. ## What to do about it Every error falls into one of these classes. The class tells you where the remedy lies; the row then names the specific action. | Class | What it means | Typical status | | - | - | - | | **Your request** | Something about the request itself was rejected. Retrying it unchanged will fail the same way. | `400`, `403`, `413`, `422` — whichever the model provider returned. | | **Your account** | The request was well-formed, but your account's own limits or balance stopped it. Nothing reached a model, and nothing was charged. | `401` unauthenticated, `402` out of credit or over a spend cap, `403` model not allowed, `429` rate limited. | | **Service-side failures** | The remedy differs per code — most clear on a retry with backoff, some need a different action. Each row says which. | `429` upstream throttle, `502`/`503` unavailable, `504` timed out, `500` ours. | | **Depends on the individual results** | One code covering several attempts. Which side is at fault is in the per-attempt detail in the response body, not in the code itself. | Whatever the underlying attempts returned. | ## Your request Something about the request itself was rejected. Retrying it unchanged will fail the same way. | `error.code` | `provider_code` | What happened | What to do | | - | - | - | - | | `bad_request` | — | The request was rejected before reaching a provider — missing required fields or an invalid combination of options. | Check required parameters and their combinations against the API reference, then retry. | | `conflict` | — | The request was well-formed, but the resource is already in a state that refuses it — granting a role the user already holds, or revoking the last super\_admin. | Re-read the resource and retry against its actual state. This is not a payload problem: the same request may succeed later, or against a different resource. | | `content_policy_violation` | `content_policy_violation` | The provider refused the request because the prompt or generated content tripped its content filter. | Revise the prompt to comply with the provider's usage policy, or try a model with a different moderation policy. | | `context_window_exceeded` | `context_window_exceeded` | The request's input tokens (plus requested output) are larger than the model can accept. | Shorten the prompt, trim conversation history, or switch to a model with a larger context window. | | `invalid_api_version` | — | The X-Mesh-Version header named a version this API does not serve, was malformed, or was sent twice with different values. | Call GET /v1/api-versions for the supported labels and send exactly one of them, or omit the header to get the baseline version. | | `invalid_input` | `invalid_input` | An attached input (image, audio, file, or data URL) was corrupt, an unsupported format, or too large. | Verify the file is a supported format and within size limits, then re-encode or re-upload it. | | `invalid_request` | `invalid_request` | One or more request parameters were malformed, out of range, or unsupported by the model. | Check the request body against the API reference — verify parameter names, types, and value ranges. | | `invalid_request_error` | — | The request body was rejected before handling — a required field is missing, the wrong type, or malformed JSON. Returned by /v1/messages, which uses the Anthropic error envelope and answers schema problems with 400 rather than 422. | Read the message for the offending field path (for example 'max\_tokens: Field required'), correct the payload, and retry. | | `model_capability_not_supported` | — | The chosen model cannot serve the requested endpoint/capability (e.g. a chat-only model asked to embed or generate images), or cannot serve the service\_tier the request asked for. | Use an endpoint the model supports, or pick a model whose capabilities match the request. A service\_tier refusal names the model: call GET /v1/models and pick one that publishes a price for that tier. See [Service Tiers](/docs/capabilities/service-tiers). | | `model_not_available_on_pinned_provider` | — | This API key is pinned to a single provider (fixed\_provider) and that provider does not serve the requested model. The pin is deliberate — the request is refused rather than routed elsewhere. | The error's details name the pinned provider and the model. Request a model that provider offers, call it on an unpinned key, or change the key's fixed\_provider. | | `model_not_found` | `model_not_found` | The requested model id is unknown to the gateway, disabled, or not permitted for this API key. | Call GET /v1/models for the exact enabled model ids, and confirm the key's allowed\_models includes it. | | `not_found` | — | The requested Mesh resource (key, template, batch, etc.) does not exist or is not visible to this key. | Verify the resource id and that it belongs to the same org/team as the key. | | `resource_not_found` | `resource_not_found` | A referenced resource (file id, batch id, previous response id, or fine-tune) does not exist or has expired. | Verify the id is correct and still valid; re-create the resource if it has expired. | | `unprocessable_entity` | — | The request was well-formed but semantically invalid — a generic user-input error with no more specific code. | Review the message detail and the request payload; correct the offending field and retry. | | `unsupported_operation` | `unsupported_operation` | The model or provider does not support the requested capability (e.g. tools, streaming, vision, this endpoint). | Remove the unsupported parameter/feature, or choose a model that supports it (see the model's capabilities). | | `validation_error` | — | FastAPI/Pydantic rejected the request body before handling — a field is missing, the wrong type, or malformed JSON. | Inspect the 'details' array in the response for the exact field paths, fix them, and retry. | ## Your account The request was well-formed, but your account's own limits or balance stopped it. Nothing reached a model, and nothing was charged. | `error.code` | `provider_code` | What happened | What to do | | - | - | - | - | | `concurrent_sessions_exceeded` | — | The account already has as many simultaneous realtime sessions as it is allowed. | Close an existing session before opening another, or request a higher concurrency limit. | | `feature_not_enabled` | — | The realtime API is not enabled for this deployment or account. | Contact support to have realtime enabled for your account. | | `forbidden` | — | The key is valid but not permitted to perform this action or reach this resource. | Use a key with the required permission, or request access from an org/team admin. | | `insufficient_credits` | `insufficient_credits` | This request used your own provider key, and that provider account has no remaining credit or has a billing problem. | Top up credits or fix the billing method on the provider account your key belongs to. | | `insufficient_quota` | — | The account behind this key has no remaining credit, so the realtime session was refused. | Add credits, then reconnect. | | `invalid_api_key` | — | The /v1/realtime socket was opened with a key that is missing, malformed, revoked or suspended. | Reconnect with an active rsk\_ key; create or rotate keys in the dashboard. | | `rate_limit_exceeded` | — | The API key exceeded a limit configured on it in Mesh — requests per minute (RPM), requests per day (RPD), or tokens per minute (TPM). Set by you or an org admin, not by the model provider. | Back off and retry after the Retry-After interval, or raise that limit on the key in Settings -> API keys. The response message names which limit was hit. | | `session_token_cap_exceeded` | — | This realtime session used the maximum tokens allowed for a single session. | Open a new session to continue; raise the per-session cap if you need longer conversations. | | `spend_limit_exceeded` | — | The API key hit its configured USD spend cap for the period. | Increase the key's spend limit or add credits to the account, then retry. | | `unauthorized` | — | The Mesh API key was missing, malformed, revoked, or does not exist. | Send a valid key as 'Authorization: Bearer rsk\_...'. Create or rotate keys in the dashboard. | | `upstream_auth_error` | `account_not_entitled` | This request used your own provider key. The credential is valid, but the provider is refusing the account it belongs to access to this model family. We did not fall back to a shared key, because the account is yours and only you can resolve it. | Check whether your account has access to this model family in the provider's console. If it already shows as granted, the block is account-level and the provider's support has to lift it — a console change will not clear it. | | `upstream_auth_error` | `invalid_auth` | This request used your own provider key, and the provider rejected that credential (401). | Verify your provider key is correct, active, and has access to the requested model; rotate it if compromised. | | `upstream_auth_error` | `permission_denied` | This request used your own provider key. The credential is valid but not authorized for this model or action (403). | Grant your provider key access to the requested model/region in the provider's console, or use a key that has it. | | `upstream_rate_limit_error` | `monthly_quota_exceeded` | This request used your own provider key, and that account has used up its allotted monthly quota. | Wait for the quota to reset or raise the quota/plan in the provider's console. | | `upstream_rate_limit_error` | `rate_limit_exceeded` | This request used your own provider key, and the provider throttled that credential (429). This is the provider's limit, not a limit set on your Mesh key. | Slow the request rate, add backoff, or request a higher rate limit from the provider. | ## Service-side failures The remedy differs per code — most clear on a retry with backoff, some need a different action. Each row says which. | `error.code` | `provider_code` | What happened | What to do | | - | - | - | - | | `api_version_migration_failed` | — | Your request is pinned to an older API version and the gateway failed while converting the response back to that version's shape. | Retry; if it persists, contact support with the request\_id. Pinning to a newer version avoids the conversion entirely. | | `auth_event_store_unavailable` | — | We could not read the account's sign-in history. This is NOT a statement that nobody signed in: "we could not tell you" and "nothing happened" are opposite facts, and this is the first one. Live session and account state is unaffected — it is read from somewhere else entirely. | Retry shortly, and never read an empty result here as evidence that no sign-in occurred. If it persists, contact support with the request\_id. Separately, this history begins when recording started, so activity from before then is unknowable rather than missing. | | `auth_service_unavailable` | — | We could not reach the service that holds sign-in sessions and connected accounts, so we cannot show or change them right now. Nothing was altered: a session you asked to end may still be active. | Retry shortly. If it persists, contact support with the request\_id. Your API keys are unaffected — they authenticate separately and keep working while this is down. | | `byok_fallback_disabled` | — | The request was routed to a provider you have configured your own API key for, that key could not serve it, and the key is set to forbid falling back to Mesh's credentials. | Check your provider key's status with the provider, or enable allow\_fallback on it (PATCH /v1/provider-keys/) to let Mesh serve the request on its own credentials instead. | | `byok_model_not_allowed` | — | The request was routed to a provider you have configured your own API key for, the model is outside that key's allowed models, and the key is set to forbid falling back to Mesh's credentials. | Add the model (or its brand, e.g. 'openai/\*') to the key's allowed\_models, or enable allow\_fallback on it, via the PATCH endpoint for that provider key. | | `connection_timeout` | — | The gateway stopped receiving heartbeats on the socket and closed it, usually a network interruption. | Reconnect; if it recurs, check for a proxy or network path that drops idle WebSockets. | | `gateway_timeout` | `asset_registration_timeout` | A reference file supplied with the request was still being prepared upstream when the request ran out of time. Generation never started. | Retry the request — a reference that has finished preparing is reused, so the retry is faster. If it keeps timing out, try a smaller reference file. | | `gateway_timeout` | `provider_timeout` | The upstream provider did not return a response within the gateway's timeout. | Retry the request; for long generations reduce max\_tokens or enable streaming. If it persists, the provider is slow. | | `idle_timeout` | — | The realtime socket was idle for longer than the allowed window and was closed. Expected behaviour, not a failure. | Reconnect when you next need the session; send periodic activity to keep one open. | | `internal_error` | — | An unhandled exception occurred inside the gateway. | Retry once; if it persists, contact support with the request\_id so we can trace the failure. | | `invalid_search_parameter` | — | A GET /v1/models/search filter or sort names something the catalog cannot honour — an unknown capability, or a price sort without exactly one priceable output modality. | Read the valid values from the response's `facets`, and pair `sort=price` with exactly one `output_modality` of text, image or video. | | `marketplace_claim_unavailable` | — | POST /v1/marketplace/claim resolves the one-time ticket the GCP Marketplace signup landing received through the gateway's Redis, and Redis did not answer. The ticket was NOT consumed and nothing about the account or the org changed. | Retry the claim shortly with the same ticket. If it keeps failing, or the ticket is older than an hour, an operator can bind the entitlement from the admin console using the account id the landing page showed. | | `model_brand_unknown` | — | A model policy — set on your organization, a team, your own account, or the API key — covers whole brands, and the requested model has no brand recorded, so the policy cannot be applied to it. | Call a model you are permitted to use, or contact support to have this model's brand recorded. | | `model_excluded` | — | A model policy does not allow the requested model — it is outside the permitted models, or explicitly excluded. The policy can be set on your organization, a team, your own account, or the API key, and every one of them must permit a model. | Call a model you are permitted to use, or ask your organization admin to allow this one. | | `model_policy_unavailable` | — | A model policy — set on your organization, a team, your own account, or the API key — restricts which models may be called, and that policy could not be evaluated for this request. | Retry in a few seconds. If it persists, contact support. | | `pii_redaction_unavailable` | — | Your key or organization has PII redaction switched on, and the redaction pass could not complete for this request — most often because the request's total text exceeded the scan ceiling. The request was refused BEFORE reaching the model provider: nothing was sent, and nothing was billed. This is deliberate. Redaction protects your data, and forwarding a request unredacted because the check failed would send personal data to a third party irreversibly. | Split the request into smaller ones, or reduce the amount of text it carries. If you would rather such requests go through UNREDACTED instead of being refused, set "on\_error": "allow" on the policy — that is an explicit, audited choice to accept the exposure. Retrying the same request unchanged will fail the same way. | | `prompt_blocked` | — | Prompt screening is switched on for your organization, and this request's text matched a category it refuses — most often a prompt-injection or jailbreak attempt. The request was refused BEFORE reaching the model provider: nothing was sent, and nothing was billed. The error's details name the categories that matched. | Review the prompt for instructions that try to override a system prompt or extract one. If the content is legitimate and the match is a false positive, contact MeshAPI support with the request id: screening is configured per organization by MeshAPI. Screening covers the text your request carries; text the gateway injects afterwards — retrieved documents, stored memories — is NOT screened, so a block always refers to something in the request you sent. | | `provider_error` | — | The upstream realtime provider returned an error mid-session. | Reconnect; if it repeats, report it with the request\_id. | | `provider_job_failed` | — | The request reached work the provider charges for and no usable result came back — a job that failed or returned nothing, a create whose outcome we could not confirm, or one we could not record. Served as 500 deliberately: 502/503/504 are back-off signals every SDK retries on, and a repeat can start a second job. The provider's own message is forwarded ahead of this explanation when it sent one. | You were not charged for this attempt. Send the request again if you want a new job; it will be charged only if it succeeds. | | `provider_not_available` | — | The model's provider is configured but its adapter is not registered (missing platform credentials) or a circuit breaker is open. | This is a server-side configuration/health issue, not your request. Retry later or use another model; escalate if it persists. | | `provider_resource_not_found` | — | The video provider no longer recognises this task id — typically because it expired on their side before it was collected. | Submit the generation again; poll and download results promptly. | | `secret_provision_failed` | — | We could not write the provider credential you supplied to our secret store, so the key was not registered. Nothing was saved, and no existing key was changed. | Retry the request. If it persists, contact support with the request\_id. | | `service_draining` | — | The instance serving your socket is being drained for a deploy, so the session was closed cleanly. | Reconnect — a new instance will accept the session immediately. | | `session_duration_exceeded` | — | The realtime socket hit the maximum lifetime for a single session and was closed. | Reconnect to continue; sessions are bounded by design. | | `shield_unavailable` | — | Prompt screening is switched on for your organization and set to refuse when it cannot run, and the screen could not complete — the screening service was unreachable, or the request's total text exceeded the screening ceiling. The request was refused BEFORE reaching the model provider: nothing was sent, and nothing was billed. Screening is NOT attempted on a truncated prompt, because screening only part of a request would miss anything placed after the cut. | Retry — an unreachable screening service is usually transient. For an over-long request, split it or reduce the text it carries. If it persists, contact MeshAPI support with the request id. | | `upstream_error` | `bad_response` | The provider returned HTTP 200 but a body the gateway could not parse (truncated or malformed). | Retry the request. If it reproduces on a specific model, report it with the request\_id so we can inspect the raw body. | | `upstream_error` | `connection_error` | A network, DNS, or TLS failure prevented the gateway from connecting to the upstream provider. | Retry shortly. If it persists across models, check the incident channel. | | `upstream_error` | `internal_provider_error` | The upstream provider returned a server-side error. | Retry with backoff. If it persists across models, check the provider's status page and our incident channel. | | `upstream_error` | `provider_overloaded` | The upstream provider reported it is overloaded / at capacity (e.g. Anthropic 529, provider 503). A request that asked for a non-standard service\_tier reports this when that tier's capacity is full. You are not charged for it. | Retry with exponential backoff, or route to an alternate model/provider for this request. On a request that asked for a non-standard service\_tier you can also retry without it to take standard capacity. See [Service Tiers](/docs/capabilities/service-tiers). | | `upstream_error` | `unknown` | The provider failed with an error the gateway could not map to a specific category. | Retry with backoff and report the request\_id — an unclassified error usually needs a new classifier entry. | | `upstream_overload` | — | The upstream provider refused the session because it is overloaded. | Retry with backoff, or try a different realtime model. | ## Depends on the individual results One code covering several attempts. Which side is at fault is in the per-attempt detail in the response body, not in the code itself. | `error.code` | `provider_code` | What happened | What to do | | - | - | - | - | | `all_models_failed` | — | POST /v1/compare fans out to several models and none returned a response, so there is nothing to compare or synthesise. This code says only that every attempt failed — WHY is in the per-model errors. | Read the per-model errors in the response body. If they name something in your request, fix that and retry; if they all report the provider unavailable, retry with backoff or narrow the model list. | ## Where it failed Failed requests in the dashboard logs carry a `failure_stage` — how far the request got before it stopped. Successful requests have none. | Stage | Meaning | | - | - | | `auth` | Authenticating the API key. | | `guard_rate_limit` | Checking your requests-per-minute / per-day limits. | | `guard_spend_cap` | Checking your USD spend caps. | | `guard_model_access` | Checking the key is allowed to call this model. | | `template` | Resolving and rendering a prompt template. | | `pii_redaction` | Removing personal data from the request before sending it. | | `balance` | Checking the account's credit balance. | | `credentials` | Resolving the upstream credential for the model. | | `upstream_submit` | Sending the request to the model provider. | | `upstream_stream` | Reading the streamed response back. | | `settlement` | Recording usage and settling the charge. | ## What errors never contain Messages are stripped of upstream internals before they reach you — SDK call frames, cloud resource identifiers and anything credential-shaped. Two consequences worth knowing: * **An error may name the provider that served the request**, in its message and in `provider_error.provider`. That names who answered this one request; it is not part of the contract, because a model is reachable through more than one upstream and we may fail over between them. * **A problem with our own upstream account reads as a provider outage.** If one of our provider credentials is rejected or throttled, you get `upstream_error` with a generic message rather than the specific cause. When you use your own provider key (BYOK) the real error is forwarded to you, because the account is yours to fix. # Models Source: https://developers.meshapi.ai/docs/reference/models Understanding model IDs, providers, capabilities, and how to choose the right model. Mesh API routes to models across multiple upstream providers. Models are identified by a `brand/model-name` slug, the same format used in all inference requests. The brand is the organization that trained the model, not the provider that serves it. Looking for the full catalog? [Models & Pricing](https://meshapi.ai/models) lists every model Mesh can route to today, with live pricing and context lengths. ## What a model is Mesh acts as a router, forwarding your standardized API calls to an expansive list of underlying foundational models. A **model** is a distinct neural network trained by an AI organization (OpenAI, Google, Anthropic, Meta, and others). Each model handles a different maximum **context length** — the number of tokens it can process in a single request. Fast, small models may have smaller limits but respond almost instantly; large models can handle whole documents and are optimized for reasoning, at higher latency and cost. When choosing one, weigh three things: * **Cost** — do you need high intelligence, or just rapid categorization? * **Latency** — lighter models offer much lower time-to-first-token. * **Context length** — passing an entire codebase or large PDF needs a long-context model. ## Model ID format ``` / ``` Examples: | Model ID | Brand | Notes | | - | - | - | | `openai/gpt-5.5` | OpenAI | GPT flagship | | `openai/gpt-4o-mini` | OpenAI | Fast, cheap | | `anthropic/claude-opus-5.5` | Anthropic | Claude flagship | | `anthropic/claude-haiku-4.5` | Anthropic | Fastest Claude | | `google/gemini-3.1-pro` | Google | Long context | | `meta-llama/llama-3.3-70b-instruct` | Meta | Open weights | | `sarvam/bulbul:v3` | Sarvam | Indian TTS | There are no provider-specific IDs such as `bedrock/...` or `vertex/...`: one ID covers every provider that serves the model, and routing picks the provider. See [Provider routing](#provider-routing). ## Listing available models ```bash theme={null} curl https://api.meshapi.ai/v1/models \ -H "Authorization: Bearer rsk_YOUR_KEY" ``` The response is a JSON array of enabled models with pricing: ```json theme={null} [ { "id": "openai/gpt-4o-mini", "name": "GPT-4o-mini", "brand": "openai", "model_type": "text", "context_length": 128000, "is_free": false, "input_modalities": ["text"], "output_modalities": ["text"], "supports_tools": true, "supports_embeddings": false, "supports_realtime": false, "supports_structured_output": true, "supports_system_prompt": true, "supports_batching": false, "supports_thinking": false, "supports_completions_api": true, "supports_responses_api": true, "pricing": { "pricing_unit": "per_1m_tokens", "prompt_usd_per_1m": "0.15000000", "completion_usd_per_1m": "0.60000000", "input_usd_per_unit": "0.1500000000", "output_usd_per_unit": "0.6000000000", "cache_read_input_usd_per_1m": "0.07500000", "effective_date": "2026-04-14", "discount_pct": null } } ] ``` The `pricing` object carries a field per billing dimension the model supports — cache read/write, long context, batch, audio, per-image, and a flat `request_usd` where one applies. Dimensions the model does not bill for are `null`. See [Pricing](/docs/getting-started/pricing) for how the units work. ### Query parameters | Parameter | Type | Description | | - | - | - | | `free` | boolean | `true` = free models only, `false` = paid only, omit = all | | `type` | string | Filter by model type: `text`, `embedding`, `image`, `audio`, `video`, `evaluation`. `tts` and `stt` models cannot be selected with this filter; read `model_type` instead | | `provider` | string | Filter by provider slug, e.g. `openai`, `vertex`, `fireworks`, `deepseek` (`amazon-bedrock` and `azure-foundry` are accepted as aliases) | ### Additional endpoints | Endpoint | Description | | - | - | | `GET /v1/models/{model_id}` | Returns a single enabled model by its full model ID (e.g. `openai/gpt-4o`) | | `GET /v1/models/{model_id}/providers` | Every provider that can serve the model, each with its own price and capabilities | | `GET /v1/models/search` | Search the catalog by name, ID, or description | The catalog is also readable without a key, at list price: `GET /v1/public/models`, `GET /v1/public/models/{model_id}`, `GET /v1/public/models/{model_id}/providers`, and `GET /v1/public/models/search`, rate-limited per IP. `GET /v1/public/brands` returns brand and provider display names and logos. ## Choosing a model ### By task type | Task | Recommended models | | - | - | | General chat | `openai/gpt-4o-mini`, `anthropic/claude-haiku-4.5` | | Complex reasoning | `openai/gpt-5.5`, `anthropic/claude-opus-5.5` | | Long documents | `google/gemini-3.1-pro` (1M context), `google/gemini-3.5-flash` | | Code generation | `openai/gpt-4o`, `meta-llama/llama-3.3-70b-instruct` | | Embeddings | `openai/text-embedding-3-small`, `cohere/embed-v4` | | Text to speech | `google/gemini-3.1-flash-tts`, `sarvam/bulbul:v3` (Indian languages) | | Image generation | `openai/gpt-image-1-mini`, `openai/gpt-image-2` | ### By cost Free models (`is_free: true`) cost \$0 for both prompt and completion tokens. They are rare: the moderation model `openai/omni-moderation-latest` is currently the only one. Check `GET /v1/models?free=true` rather than planning around them. Paid model pricing varies widely — a frontier reasoning model may cost 100× more per token than a lightweight model. Check `pricing.prompt_usd_per_1m` and `pricing.completion_usd_per_1m` in the API response for live pricing. ### Let the gateway decide Set `"model": "auto"` and the gateway classifies your prompt and picks an appropriate model automatically. See [Auto Routing](/docs/capabilities/auto-routing). ## Model capabilities | Field | Type | Meaning | | - | - | - | | `id` | string | Full model ID, `brand/model-name` | | `brand` | string | The organization that trained the model, e.g. `openai`, `anthropic` | | `context_length` | integer | Maximum combined prompt + completion tokens | | `model_type` | string | `text`, `embedding`, `image`, `audio`, `tts`, `stt`, `video`, or `evaluation` | | `input_modalities` | string\[] | Accepted input types (e.g. `["text", "image"]`) | | `output_modalities` | string\[] | Produced output types (e.g. `["text"]`) | | `supports_tools` | boolean | Supports tool/function calling | | `supports_embeddings` | boolean | Can produce embedding vectors | | `supports_realtime` | boolean | Supports WebSocket realtime sessions | | `supports_structured_output` | boolean | Supports JSON schema-constrained output | | `supports_system_prompt` | boolean | Accepts a system message | | `supports_batching` | boolean | Eligible for async batch requests | | `supports_flex` | boolean | Can be served on the `flex` service tier | | `supports_thinking` | boolean | Exposes chain-of-thought reasoning tokens | | `supports_completions_api` | boolean | Compatible with `/v1/chat/completions` | | `supports_responses_api` | boolean | Compatible with `/v1/responses` | | `supports_video_generation` | boolean | Can generate video | | `supports_video_*` | boolean | One flag per kind of input a video model accepts — `first_frame`, `last_frame`, `reference_image`, `reference_video`, `reference_audio` — plus `negative_prompt` for the parameter of that name. Read them before sending an input a model may not take: see [Video Generation](/docs/capabilities/video-generation#finding-out-what-a-model-accepts) | | `supports_background_response` | boolean | Eligible for `background: true` on `/v1/responses` | | `supports_image_*` | boolean | Image edit capabilities — `edit`, `inpaint`, `outpaint`, `mix`, `reframe`, `upscale`, `reference`, `remove_background` | | `image_operations` | string\[] | The image operations a request can actually use on this model. Prefer it over the `supports_image_*` flags, which can be set for an operation the serving provider rejects with `422` | | `supports_generate_audio` | boolean \| null | Whether a video model can generate an audio track | | `max_prompt_chars` | integer \| null | Longest prompt the provider accepts; `null` means no limit is recorded, not that there is none | | `context_window` / `standard_context_threshold` | integer \| null | The provider's context window, and the prompt size above which long-context pricing applies (`null` = no long-context tier) | | `realtime_session_max_tokens` / `realtime_max_concurrent_per_owner` | integer \| null | Per-session token cap and concurrent-session limit for realtime models | | `is_composite` / `composite_models` | boolean / string\[] | Whether the entry fans out to several underlying models, and which | | `is_free` | boolean | \$0 cost for all tokens | | `deprecation` | object \| null | Set when the model is deprecated: `deprecated_on`, `sunset_on` (the announced end date, `null` until announced), and `replacement_model_id`. The model stays usable while deprecated | `supports_system_prompt` defaults to `true`. Check the relevant flag before sending a request that depends on a specific capability — an unsupported feature is generally rejected by the upstream rather than silently downgraded. ## Provider routing A model ID is not tied to one upstream. Many models are served by several providers; `anthropic/claude-sonnet-4.5`, for example, runs on Anthropic, AWS Bedrock, and others. Resilient routing can fall back between them when one fails. `GET /v1/models` and `GET /v1/models/{model_id}` show the price a bare model ID bills at; `GET /v1/models/{model_id}/providers` lists every provider that can serve the model, alphabetically, each at its own price. Which provider is the default is not published, so do not depend on a specific one. Provider slugs (`openai`, `bedrock`, `vertex`, `azure_foundry`, `fireworks`, `together`, and others) appear in the `provider` filter above, in `/providers` rows, and in [BYOK](/docs/capabilities/byok); `GET /v1/public/brands` lists them all with display names. The model registry is live — models can be enabled or disabled by the platform team. Always use `GET /v1/models` at runtime rather than hardcoding a static model list. # Support Source: https://developers.meshapi.ai/docs/reference/support Get help with Mesh API — debugging tips, contact options, and useful resources. ## Self-service resources Before reaching out, these resources often resolve issues quickly: * **Request logs** — every request has a unique `req_...` ID visible in Dashboard → **Logs**. Filter by date, model, or key to find a specific call. * **Error codes** — check the `error.code` field in the response body. Common codes are listed below. * **Model status** — if a specific model is failing, check whether it's enabled in `GET /v1/models`. * **Balance** — a `402 Payment Required` response means your balance is zero or a spend cap was hit. Check Dashboard → **Billing**. ## Common error codes | Code | HTTP | Cause | Fix | | - | - | - | - | | `unauthorized` | 401 | Missing or invalid API key | Check `Authorization: Bearer rsk_...` header | | `forbidden` | 403 | Key is active but lacks permission | Verify key scopes | | `spend_limit_exceeded` | 402 | Balance zero or spend cap exceeded | Top up or raise the cap | | `rate_limit_exceeded` | 429 | RPM or RPD limit hit | Wait and retry, or raise the key's rate limits | | `model_not_found` | 404 | Model ID doesn't exist or isn't enabled | Use `GET /v1/models` to check available models | | `upstream_error` | 503 | Provider returned an error | Retry — often transient | | `upstream_rate_limit_error` | 429 | Provider throttled the request | Retry with backoff | | `model_excluded` | 403 | A model policy (org, team, member or key) excludes this model | Use an allowed model, or ask an org admin to allow it | | `invalid_api_version` | 400 | The `X-Mesh-Version` label is malformed or names no version | Send a published version label. See [API Versioning](/docs/reference/api-versioning) | | `insufficient_credits` | 402 | Your own provider (BYOK) account is out of credit | Top up the provider account the key belongs to | ## The error catalog endpoint `GET /v1/errors` returns the full error action-item catalog — the machine-readable source behind the table above. Use it to surface a helpful message and next step for any error your client receives: ```bash theme={null} curl https://api.meshapi.ai/v1/errors \ -H "Authorization: Bearer rsk_YOUR_KEY" ``` Resolve an error by looking up `codes` with the specific error identifier, falling back to `categories` by the error's category, then to `default`. The payload is small and slow-changing. Cache it, and re-fetch only when its `version` field changes. For a symptom-by-symptom walkthrough of Mesh API failures, see [Troubleshooting → Mesh API](/debug/mesh-api). ## Contacting support **Email:** [support@meshapi.ai](mailto:support@meshapi.ai) When writing in, include: 1. Your **request ID** (`req_...` from the response headers or Dashboard logs) 2. The **model** and **endpoint** you were calling 3. A **description of the issue** and any error message you received 4. The **account email** associated with your API key ## Billing issues For payment failures, missing credits, or invoice questions: [support@meshapi.ai](mailto:support@meshapi.ai) with subject line **Billing**. ## Feature requests Have an idea? Open a discussion or request at [support@meshapi.ai](mailto:support@meshapi.ai) with subject line **Feature Request**. # Usage & Monitoring API Source: https://developers.meshapi.ai/docs/reference/usage-api Pull your usage, spend, rate-limit, and balance data programmatically — the same numbers the dashboard shows. The usage endpoints expose **usage, spend, rate-limit, and balance data programmatically**. Each authenticates with an `rsk_` API key, so anything you can see in the console you can also pull from your own code. | What | Endpoint | | - | - | | Requests / tokens / spend aggregates | `POST /v1/usage` | | Per-request history | `POST /v1/usage/events` | | One request by its `X-Request-Id` | `GET /v1/usage/requests/{request_id}` | | Live RPM / RPD / TPM vs limits | `GET /v1/usage/rate-limits` | | Remaining credit balance (with reserved breakdown) | `GET /v1/balance` | Full request/response schemas and an interactive explorer are in the **API Reference** tab. This page is the guide. For the `usage` block on each completion response, see [Usage Object](/docs/reference/usage-object). CSV export (`GET /v1/usage/events/export`) is dashboard-only — it authenticates the signed-in session and is not callable with an API key. Forecasting / usage prediction is not part of this API yet. ## Authentication & scoping | Credential | Header | Scope | | - | - | - | | **API key** | `Authorization: Bearer rsk_` | That key's own usage. `key_id` / `team_id` / member filters are ignored. Spend trend is the exception: it returns org-wide spend. | On `POST /v1/usage`, `POST /v1/usage/events`, and the request lookups, an API key reads only the usage of the key it authenticated with. The server sets that scope, and request parameters cannot widen it. The dashboard's own views are org-scoped instead, which is why the `org` / `team` / `member` scopes appear in these schemas. `GET /v1/usage/spend-trend` does not narrow to the key. Any member's `rsk_` key gets the whole org's spend, or one team's spend with `team_id` (the key's owner must belong to that team). `org_id` is required on `POST /v1/usage`, `POST /v1/usage/events`, and `GET /v1/usage/spend-trend`. Leave it out and the call fails with `422`. The usage and events endpoints ignore its value for an API key, so any placeholder works. Spend trend needs the key's real organization ID; read it as `org_id` from `GET /v1/keys`, which takes an [admin key](/docs/getting-started/admin-keys) (`$MESH_ADMIN_KEY`), not an `rsk_` key. ## Usage summary — `POST /v1/usage` Aggregate requests, tokens, and spend, plus a per-model breakdown. Filters are a JSON body (the array filters are more natural as a body than query params). ```bash theme={null} curl https://api.meshapi.ai/v1/usage \ -H "Authorization: Bearer rsk_" \ -H "Content-Type: application/json" \ -d '{"org_id": "", "since": "2026-07-01", "until": "2026-07-14", "limit": 10}' ``` | Body field | Notes | | - | - | | `org_id` | Required. For an API key any value works, because the scope comes from the key itself. | | `since` / `until` | ISO 8601. Bare dates cover the whole day. | | `model` | `string[]` — filter by model ids. | | `status` | `string[]`, e.g. `success`, `error`. | | `model_type` | `string[]` — filter by modality, e.g. `image`, `video`. | | `service_tier` | `string[]` — the tier that served, e.g. `flex`. | | `failure_stage` | `string[]` — failures in one pipeline stage, e.g. `upstream_stream`. | | `routing` | `string[]` — requests routing did not serve on the first attempt: `retried`, `provider_fallback`, `model_fallback`. Unknown values are a `422`. | | `end_user_id` | Narrow to one end-user identifier. | | `request_id` | Narrow to a single request. | | `tags` / `tag_key` | Rows carrying all of these request tags; group the summary by one tag key. | | `key_id` / `team_id` / `member_user_id` | Dashboard scopes, ignored for API keys. | | `limit` / `offset` | Paginate the `by_model` breakdown (`limit` ≤ 200). | ```json theme={null} { "total_requests": 1284, "successful_requests": 1270, "error_requests": 14, "prompt_tokens": 482913, "completion_tokens": 197233, "total_tokens": 680146, "total_cost_usd": "12.4832100", "by_model_total": 3, "by_model": [ { "model": "openai/gpt-4o-mini", "requests": 900, "total_tokens": 420000, "cost_usd": "6.1200000" } ] } ``` Totals span the full range; `by_model` is one page (`by_model_total` is the full distinct-model count). Monetary values are strings to preserve decimal precision. ## Per-request history — `POST /v1/usage/events` Paginated list of individual requests, newest first. Same filters as the summary, plus `limit` (≤ 200) / `offset`. ```bash theme={null} curl https://api.meshapi.ai/v1/usage/events \ -H "Authorization: Bearer rsk_" \ -H "Content-Type: application/json" \ -d '{"org_id": "", "status": ["success"], "limit": 20}' ``` ```json theme={null} { "events": [ { "id": "0190a1c0-0000-7000-8000-00000000e1e1", "request_id": "req_01ARZ3NDEKTSV4RRFFQ69G5FAV", "model": "openai/gpt-4o-mini", "prompt_tokens": 128, "completion_tokens": 64, "total_tokens": 192, "cost_usd": "0.0000480", "latency_ms": 812, "status": "success", "created_at": "2026-07-14T10:15:30+00:00" } ], "total": 1284, "limit": 50, "offset": 0 } ``` Retried "waste" attempts are collapsed — each request appears once (the served attempt). A cross-provider routing fallback surfaces as the boolean `routing_fallback`. ### One request — `GET /v1/usage/requests/{request_id}` Look up a single request by the id returned in its `X-Request-Id` response header: the model and provider that served it, status, latency, tokens, cost, and every attempt made. A request refused before any model was called (rate limit, spend cap, empty balance, model access) has no usage row and comes back with `outcome: "rejected"`. ```bash theme={null} curl https://api.meshapi.ai/v1/usage/requests/ \ -H "Authorization: Bearer rsk_" ``` Ids resolve for **90 days** — see [How far back data goes](#how-far-back-data-goes), which is the one place on this page where a window applies. An older id, or one made by a different key, returns `404`. Requests to `/v1/chat/compare` and `/v1/batches` are not resolvable here. Use `GET /v1/batches/{batch_id}` for a batch. ### Per-attempt timeline — `GET /v1/usage/events/{event_id}/attempts` When a request was retried or rerouted, this returns every attempt that shares its `request_id` — the served row plus each retried row, oldest first. It is how you see every model that was tried behind one collapsed event. ```bash theme={null} curl https://api.meshapi.ai/v1/usage/events//attempts \ -H "Authorization: Bearer rsk_" ``` Authorization mirrors the events list: an API-key caller sees only events made by that key. The same scrubbing applies — no admin fields — and a failed attempt's `error_message` may name the provider that answered it. ## Spend trend — `GET /v1/usage/spend-trend` Historical spend for the key's whole org, not only the key, bucketed by `granularity`. The response adds a trailing 3-bucket moving average and the window's total. ```bash theme={null} curl "https://api.meshapi.ai/v1/usage/spend-trend?org_id=&granularity=monthly" \ -H "Authorization: Bearer rsk_" ``` | Parameter | Notes | | - | - | | `org_id` | Required. The key's own org. Any other value is a `403`. | | `team_id` | Optional. Narrow to one team the key's owner belongs to. | | `granularity` | `hourly` (last 24 hours, hourly buckets), `weekly` (last 12 weeks, weekly buckets), or `monthly` (last 30 days, daily buckets; the default). | | `force_refresh` | Bypass the cached result. | ## Rate limits — `GET /v1/usage/rate-limits` Live RPM / RPD / TPM counters vs. the effective limits (always fresh, never cached). For an API-key caller only the `key` scope is populated. ```bash theme={null} curl https://api.meshapi.ai/v1/usage/rate-limits \ -H "Authorization: Bearer rsk_" ``` ```json theme={null} { "as_of": "2026-07-14T10:20:00+00:00", "org": null, "team": null, "member": null, "key": { "rpm": { "current": 42, "limit": 60, "is_default_limit": false, "pct": 70.0, "resets_in_seconds": 37 }, "rpd": { "current": 5120, "limit": 10000, "is_default_limit": false, "pct": 51.2, "resets_in_seconds": 49200 }, "tpm": { "current": 18000, "limit": 20000000, "is_default_limit": true, "pct": 0.09, "resets_in_seconds": 37 } } } ``` Each metric carries `current`, the effective `limit`, `pct` (0–100), and `resets_in_seconds`. A limit the key does not explicitly set is shown as the system default with `is_default_limit: true` (the TPM default is display-only — keys with no explicit `tpm_limit` are not TPM-throttled). Use `pct` to draw a usage bar and warn as it nears 100. The dashboard sees the `org` / `team` / `member` scopes instead of the single `key` scope. ## Balance — `GET /v1/balance` Current credit balance, plus how much is **reserved** by in-flight operations (realtime sessions, async background jobs, video generation) and therefore not spendable. ```bash theme={null} curl https://api.meshapi.ai/v1/balance \ -H "Authorization: Bearer rsk_" ``` ```json theme={null} { "balance_usd": "42.5000000", "reserved_usd": "15.0000000", "available_usd": "27.5000000", "reserved_breakdown": [ { "kind": "realtime", "reserved_usd": "5.0000000" }, { "kind": "video", "reserved_usd": "10.0000000" } ], "updated_at": "2026-07-14T10:15:30+00:00" } ``` With an org context the **org owner's** balance is returned — all org members share one billing pool; a personal key with no org gets its own balance. `available_usd` = `max(0, balance_usd − reserved_usd)`; `reserved_breakdown` lists only the categories currently holding funds and sums to `reserved_usd`. ## How far back data goes Not every endpoint on this page reaches back the same distance, and the difference is deliberate: usage and spend are billing records, while prompts and completions are your content. | What | How far back | | - | - | | Usage aggregates — `POST /v1/usage` | **No limit.** Your full account history | | Per-request history — `POST /v1/usage/events` | **No limit** | | Spend trend — `GET /v1/usage/spend-trend` | **No limit** | | Balance — `GET /v1/balance` | Current, with its own ledger history | | One request by id — `GET /v1/usage/requests/{request_id}` | **Up to 90 days**, then `404` | | Request and response **bodies** on that lookup | **Up to 90 days** | | Refused requests — `POST /v1/usage/rejections` | **Up to 90 days** | | Audit log | 365 days | | Webhook delivery history | 30 days | **Spend and usage history has no cutoff.** You can pull last quarter or last year from `POST /v1/usage` and `POST /v1/usage/events` — requests, tokens, cost, model, status and latency, per request, for as far back as your account goes. If you are reconciling an invoice or charting a year of spend, these are the endpoints to use. **Two things are capped at 90 days: looking up one request by its id, and refused requests.** Past that the lookup returns `404` whether or not the request existed, and request and response bodies are not available at all. Refused requests — the ones rejected before reaching a model — have the **same cap**, so they age out of `POST /v1/usage/rejections` while the served requests around them stay in `POST /v1/usage/events` indefinitely. **90 days is a ceiling, not a floor.** It is the furthest these two will reach, and today they reach less: the request log holds data from 6 August 2026 onward, because it was re-partitioned by month with 30 days of history carried over and the older partition retired. Anything before that is archived and not available through the API. The reachable window grows day by day until it meets the 90-day cap in early November 2026, after which it is a rolling 90 days. Treat a `404` or an absent body as "not available", never as "this request did not happen". The cap on the lookup is not an accident of storage — it is why the response carries `retention_days`: ```json theme={null} { "request_id": "req_...", "outcome": "success", "retention_days": 90 } ``` Read that field rather than hard-coding 90, so a change to the window does not silently break your integration. Only the **bodies** and the single-request lookup stop being available. The per-request metadata — model, tokens, cost, status, latency, your `request_tags` — stays available from `POST /v1/usage/events` for as long as your account exists, so it needs no archiving on your side. If you need the request and response bodies beyond the window, capture them inside it. These windows describe what this API will **return**, not a deletion schedule, and they are not a data-retention commitment. Bodies are also returned only where the key that served the request had body logging enabled. ## Notes * **Money** is always a decimal string (never a float). * **Caching:** API-key reads are computed fresh; the dashboard's org-scoped summary/events reads are cached \~60s (`?refresh=true` bypasses). Rate-limits and balance are never cached. * **Errors** use the standard envelope — `401` (bad credential), `403` (not an org member), `429` (rate limited). # Usage Object Source: https://developers.meshapi.ai/docs/reference/usage-object The usage block on Chat Completions, Responses and Messages: the fixed keys, provider keys, which cache counts each provider reports, and how to read a gateway cache hit. Every successful completion returns a `usage` object that counts the tokens the request consumed. On each of `POST /v1/chat/completions`, `POST /v1/responses` and `POST /v1/messages`, the usage carries a **fixed set of keys** in that endpoint's own format, whichever provider served the request. The **values** in it, the cache counts especially, still depend on what that provider reports. Keys the serving provider adds on its own are returned alongside the fixed ones — see [Provider keys](#provider-keys). The per-provider behaviour on this page was observed on 8 October 2026. It follows the **serving provider**, so it can change when a provider changes its API or when MeshAPI routes a model differently. To see which provider served a request, read `event.provider` from [`GET /v1/usage/requests/{request_id}`](/api/usage/get-request). ## Chat Completions usage Every chat completion returns these keys: ```json theme={null} { "prompt_tokens": 4701, "completion_tokens": 29, "total_tokens": 4730, "prompt_tokens_details": { "cached_tokens": 4686, "cache_write_tokens": 0, "cache_write_1h_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 }, "byok_used": false, "fallback_triggered": false } ``` The same keys appear on streamed and non-streamed responses. A count that is unused, or that the provider doesn't report, is `0`. | Field | Meaning | | - | - | | `prompt_tokens` | All input tokens, **including** tokens read from or written to the prompt cache | | `completion_tokens` | Output tokens | | `total_tokens` | `prompt_tokens + completion_tokens` | | `prompt_tokens_details.cached_tokens` | Input tokens read from the provider's prompt cache | | `prompt_tokens_details.cache_write_tokens` | Input tokens written to the prompt cache, all TTLs combined | | `prompt_tokens_details.cache_write_1h_tokens` | The part of `cache_write_tokens` written with a 1-hour TTL | | `prompt_tokens_details.audio_tokens` | Audio input tokens | | `completion_tokens_details.reasoning_tokens` | Tokens the model spent reasoning, where the provider reports them | | `completion_tokens_details.audio_tokens` | Audio output tokens | | `completion_tokens_details.accepted_prediction_tokens` / `rejected_prediction_tokens` | Predicted-output tokens that were accepted / rejected | | `byok_used` | `true` if the request was served with your own provider key — see [BYOK](/docs/capabilities/byok) | | `fallback_triggered` | `true` if MeshAPI fell back to another provider or model to serve the request — see [Retry and fallback](/docs/platform/retry-and-fallback) | **Deprecated:** some responses also carry a top-level `cache_creation_input_tokens`. It duplicates `prompt_tokens_details.cache_write_tokens` and appears only on a cache write served by Bedrock, for example on Anthropic Claude or Amazon Nova models. Read `cache_write_tokens` instead. ### Provider keys Besides the fixed keys, `usage` carries any other key the serving provider reported, unchanged. For example: ```json theme={null} { "prompt_tokens": 1000, "completion_tokens": 10, "total_tokens": 1010, "prompt_tokens_details": { "cached_tokens": 640, "cache_write_tokens": 0, "cache_write_1h_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 }, "byok_used": false, "fallback_triggered": false, "prompt_cache_hit_tokens": 640, "prompt_cache_miss_tokens": 360 } ``` Here `prompt_cache_hit_tokens` and `prompt_cache_miss_tokens` come from DeepSeek; the same count is also in `cached_tokens`. Other examples are OpenRouter's `is_byok` and xAI's `num_sources_used`. Provider keys can also appear inside `prompt_tokens_details` and `completion_tokens_details`. * **Provider keys depend on the route.** They differ by provider and can change or disappear when a provider changes its API or MeshAPI routes a model differently. Build on the fixed keys. * **A fixed key always holds MeshAPI's value.** If a provider sends a key with the same name, MeshAPI's value replaces it. OpenRouter's `is_byok` describes OpenRouter's own account. Read `byok_used` for whether your provider key served the request. * **Provider cost figures are never returned.** Read your charge from the Usage API instead — see the note under [Summary](#summary). The same rule applies on [Responses](#responses-api) and [Messages](#messages-api). ### What a `0` means The keys are always present, but `0` can mean either "nothing happened" or "the provider doesn't report this": * A route that doesn't report cache writes returns `cache_write_tokens: 0` even when the provider cached the prompt. * A route that doesn't report the 1-hour split returns `cache_write_1h_tokens: 0` even after a 1-hour write. The next section shows which routes report what. ## Cache reporting by serving provider Which cache counts are filled in on `POST /v1/chat/completions`, by the provider that served the request: | Serving provider | Models | Reports cache writes | Reports cache reads | Reports 1h writes separately | | - | - | - | - | - | | Bedrock (Anthropic) | `anthropic/claude-sonnet-4.6`, `anthropic/claude-haiku-4.5` | Yes | Yes | Yes | | Bedrock (Amazon Nova) | `amazon/nova-pro-v1` | Yes | Yes | No | | Bedrock (other models) | `moonshotai/kimi-k2.5`, `meta-llama/llama-4-scout` | No | No | No | | OpenAI | `openai/gpt-5.6-terra` | Yes | Yes | n/a (no 1h TTL) | | OpenAI | `openai/gpt-4o-mini`, `openai/gpt-5.4-mini` | No | Yes | n/a (no 1h TTL) | | Google (Vertex) | `google/gemini-2.5-flash` | No | Yes | No | | DeepInfra | `deepseek/deepseek-v4-flash` | No | Yes | No | | DeepInfra | `google/gemini-3.1-pro` | No | No | No | | xAI | `x-ai/grok-4.3` | No | Yes | No | | Qwen | `qwen/qwen3-max` | No | Yes | No | | Mistral | `mistralai/mistral-large-2512` | No | Yes | No | | OpenRouter | `cohere/command-a` | No | No | No | * **"No"** means the count is always returned as `0` on that route. The provider may still cache; it just isn't reported. * **1-hour writes on Amazon Nova** are counted in `cache_write_tokens`, but `cache_write_1h_tokens` stays `0`. * **Cache reads are best-effort** on providers that cache automatically (OpenAI, Google, DeepInfra, xAI, Qwen, Mistral). An identical repeat can still return `cached_tokens: 0`, and OpenAI can report a fresh write instead. Which routes honour a `cache_control` marker in the first place is covered in [Caching](/docs/capabilities/caching#where-markers-are-honoured). ### Examples These are real usage blocks, reformatted only. ```json theme={null} { "prompt_tokens": 4701, "completion_tokens": 29, "total_tokens": 4730, "prompt_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 4686, "cache_write_1h_tokens": 4686, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 }, "byok_used": false, "fallback_triggered": false, "cache_creation_input_tokens": 4686 } ``` ```json theme={null} { "prompt_tokens": 4305, "completion_tokens": 24, "total_tokens": 4329, "prompt_tokens_details": { "cached_tokens": 4302, "cache_write_tokens": 0, "cache_write_1h_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 }, "byok_used": false, "fallback_triggered": false } ``` ```json theme={null} { "prompt_tokens": 4493, "completion_tokens": 21, "total_tokens": 4514, "prompt_tokens_details": { "cached_tokens": 4480, "cache_write_tokens": 0, "cache_write_1h_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 161, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 }, "byok_used": false, "fallback_triggered": false } ``` ## Splitting input tokens `prompt_tokens` includes cached and cache-written tokens: ```text theme={null} fresh input tokens = prompt_tokens - cached_tokens - cache_write_tokens 1-hour cache write = cache_write_1h_tokens other cache write = cache_write_tokens - cache_write_1h_tokens ``` In the 1-hour write example above: 4701 − 0 − 4686 = 15 fresh tokens, and 4,686 tokens written with a 1-hour TTL. On Anthropic models, "other cache write" is the 5-minute TTL. What each kind of token costs is in [What it costs](/docs/capabilities/caching#what-it-costs). ## Streaming On a streamed chat completion, the usage object arrives on the **final chunk**. You don't need `stream_options.include_usage`: MeshAPI requests usage from the provider on every stream. The keys are the same as on a non-streamed response. On a Responses stream, usage arrives on the closing `response.completed` or `response.incomplete` event, under `response.usage`. On a Messages stream, it arrives on `message_delta`. If the upstream fails partway through a stream, the response can end without a usage chunk. ## Gateway cache hits When the MeshAPI [response cache](/docs/capabilities/caching#gateway-response-cache) answers a request: * the response carries the header `X-Cache: HIT` * the request is **not billed** — its `event.cost_usd` from [`GET /v1/usage/requests/{request_id}`](/api/usage/get-request) is `null` The usage object on a hit is **not** a count of new work: | Endpoint | Usage on a gateway hit | | - | - | | `/v1/chat/completions` | `prompt_tokens`, `completion_tokens` and `total_tokens` from the original response. All cache counts are `0`, and `cache_creation_input_tokens` is omitted. | | `/v1/responses` | `input_tokens`, `output_tokens` and `total_tokens` from the original response. All cache counts are `0`. | | `/v1/messages` | `input_tokens` holds the original request's **entire** input, including the cached part. The cache counts are `0`. | Check `X-Cache` before using usage for cost tracking. To always reach the provider, send `X-Mesh-Cache: no-store`. A Chat Completions hit on `anthropic/claude-haiku-4.5`, where the original response reported `cache_write_tokens: 4687`: ```json theme={null} { "prompt_tokens": 4702, "completion_tokens": 26, "total_tokens": 4728, "prompt_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0, "cache_write_1h_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 }, "byok_used": false, "fallback_triggered": false } ``` ## Responses API Every [`POST /v1/responses`](/docs/capabilities/responses-api) response returns these keys, in the OpenAI Responses format: ```json theme={null} { "input_tokens": 4305, "input_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 4302, "cache_write_1h_tokens": 0, "audio_tokens": 0 }, "output_tokens": 25, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 4330, "byok_used": false, "fallback_triggered": false } ``` The fields mean the same as their Chat Completions counterparts: | Field | Same as on Chat Completions | | - | - | | `input_tokens` | `prompt_tokens`. It **includes** cache reads and writes. | | `input_tokens_details.*` | `prompt_tokens_details.*` | | `output_tokens` | `completion_tokens` | | `output_tokens_details.reasoning_tokens` | `completion_tokens_details.reasoning_tokens` | | `total_tokens` | `total_tokens`, as the serving provider reported it | | `byok_used`, `fallback_triggered` | The same | Provider keys are returned alongside, as on [Chat Completions](#provider-keys). What each serving provider reports: ```json theme={null} { "input_tokens": 4305, "input_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 4302, "cache_write_1h_tokens": 0, "audio_tokens": 0 }, "output_tokens": 25, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 4330, "byok_used": false, "fallback_triggered": false } ``` On this endpoint, an identical repeat can report another full cache write rather than a cache read. Claude models are answered through MeshAPI's translation, so they carry the same cache breakdown as on Chat Completions. This is a cache write with a 1-hour TTL: ```json theme={null} { "input_tokens": 4701, "input_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 4686, "cache_write_1h_tokens": 4686, "audio_tokens": 0 }, "output_tokens": 29, "output_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 }, "total_tokens": 4730, "byok_used": false, "fallback_triggered": false } ``` A model answered through translation also carries the other Chat Completions counts in `output_tokens_details`. `output_tokens` includes `reasoning_tokens` (221 = 21 + 200), and xAI's own keys are returned alongside: ```json theme={null} { "input_tokens": 4491, "input_tokens_details": { "cached_tokens": 128, "cache_write_tokens": 0, "cache_write_1h_tokens": 0, "audio_tokens": 0 }, "output_tokens": 221, "output_tokens_details": { "reasoning_tokens": 200 }, "total_tokens": 4712, "byok_used": false, "fallback_triggered": false, "num_sources_used": 0, "num_server_side_tools_used": 0, "context_details": { "input_tokens": 4491, "output_tokens": 229 } } ``` Not every route can serve the Responses API. One that can't returns `501 model_capability_not_supported` — for example, `google/gemini-2.5-flash` on the Vertex route. `GET /v1/responses/{id}` returns the stored response as the provider kept it, so its usage is not in this format. ## Messages API Every [`POST /v1/messages`](/docs/capabilities/messages-api) response returns these keys, in Anthropic's format: ```json theme={null} { "input_tokens": 15, "cache_creation_input_tokens": 4686, "cache_read_input_tokens": 0, "cache_creation": { "ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 4686 }, "output_tokens": 29, "byok_used": false, "fallback_triggered": false } ``` | Field | Meaning | | - | - | | `input_tokens` | Only **uncached** input | | `cache_creation_input_tokens` | Input tokens written to the prompt cache, all TTLs combined | | `cache_read_input_tokens` | Input tokens read from the prompt cache | | `cache_creation.ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens` | `cache_creation_input_tokens`, split by TTL | | `output_tokens` | Output tokens | | `byok_used`, `fallback_triggered` | As on Chat Completions | All of these are present on every response, and `0` when unused. A cache read: ```json theme={null} { "input_tokens": 15, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 4689, "cache_creation": { "ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0 }, "output_tokens": 29, "byok_used": false, "fallback_triggered": false } ``` Total input is `input_tokens + cache_creation_input_tokens + cache_read_input_tokens`. Don't apply this on a [gateway cache hit](#gateway-cache-hits), where `input_tokens` already holds the whole input. On a stream, the counts arrive on `message_delta`. `message_start` carries the same fixed keys as zeros — see [Messages API](/docs/capabilities/messages-api). Provider keys are returned on the response body and on `message_delta`. ## Summary | | Chat Completions | Responses API | Messages API | | - | - | - | - | | Usage format | Chat Completions, fixed keys | OpenAI Responses, fixed keys | Anthropic Messages, fixed keys | | Input total | `prompt_tokens` (includes cache reads and writes) | `input_tokens` (includes cache reads and writes) | `input_tokens + cache_creation_input_tokens + cache_read_input_tokens` | | Cache read | `prompt_tokens_details.cached_tokens` | `input_tokens_details.cached_tokens` | `cache_read_input_tokens` | | Cache write | `prompt_tokens_details.cache_write_tokens` | `input_tokens_details.cache_write_tokens` | `cache_creation_input_tokens` | | 1-hour cache write | `prompt_tokens_details.cache_write_1h_tokens` | `input_tokens_details.cache_write_1h_tokens` | `cache_creation.ephemeral_1h_input_tokens` | | Reasoning | `completion_tokens_details.reasoning_tokens` | `output_tokens_details.reasoning_tokens` | Not returned | | Provider keys | Returned alongside | Returned alongside | Returned alongside | | Gateway cache hit | `X-Cache: HIT`, not billed, cache counts `0` | `X-Cache: HIT`, not billed, cache counts `0` | `X-Cache: HIT`, not billed, whole input in `input_tokens` | Usage tells you what was processed, not what you were charged. For the exact charge of a request, read `event.cost_usd` from [`GET /v1/usage/requests/{request_id}`](/api/usage/get-request). That endpoint returns no per-request cache fields, so record `usage` from the response if you need them. ## Related * [Caching](/docs/capabilities/caching) — the gateway response cache and provider prompt caching, and what each costs * [Usage & Monitoring API](/docs/reference/usage-api) — usage, spend and per-request history * [Pricing](/docs/getting-started/pricing) — how cached tokens are billed # Welcome to Mesh API Source: https://developers.meshapi.ai/index One key, all AI models. The unified gateway for your AI infrastructure. Mesh API is an intelligent AI model gateway that provides a unified, OpenAI-compatible interface for accessing LLMs from dozens of providers. One **Router Service Key** (`rsk_...`) gives you access to the world's best models — with built-in billing, logging, rate limits, and performance optimizations. You don't need to manage multiple API keys, learn different SDKs, or rebuild your integration every time a new model drops. Drop Mesh in as a base-URL replacement and get everything else for free. ## What you can do OpenAI-compatible chat completions across 500+ text models in a catalog of 1,300+, plus the Responses API for controllable reasoning effort and provider-hosted tools. Vision, image and video generation, text-to-speech, speech-to-text, and bidirectional realtime voice. Upload and search your own documents, generate embeddings, and attach durable per-user memory to any request. Per-key spend caps, rate limits, a free response cache, batch pricing, and usage analytics down to the request. ## Why Mesh? * **One universal API** — code once. A single chat-completions request works across every model in the catalog. * **Unified billing** — deposit funds into one account instead of managing dozens of provider subscriptions. * **Zero-downtime fallbacks** — automatically retry and reroute around provider outages. See [Retry & Fallback](/docs/platform/retry-and-fallback). * **Cost optimization** — a free 24-hour response cache, provider prompt caching, batch pricing, and real-time price discovery. * **Security** — API keys with their own spend caps and rate limits, PII redaction, and zero data retention you can turn on for your org. * **Drop-in replacement** — works with any OpenAI-compatible SDK, no code changes needed. ## How it works 1. **Authenticate** — create a single API key in the dashboard. 2. **Integrate** — replace the base URL of any OpenAI-compatible SDK with `https://api.meshapi.ai/v1`. 3. **Execute** — pass the model ID and standard parameters as you normally would. Mesh routes the request to the underlying provider transparently. ## Integration at a glance ```python theme={null} from openai import OpenAI client = OpenAI( api_key="rsk_...", base_url="https://api.meshapi.ai/v1", ) response = client.chat.completions.create( model="openai/gpt-4o", messages=[{"role": "user", "content": "Hello!"}], ) ``` ```typescript theme={null} import OpenAI from "openai"; const client = new OpenAI({ apiKey: "rsk_...", baseURL: "https://api.meshapi.ai/v1", }); const response = await client.chat.completions.create({ model: "openai/gpt-4o", messages: [{ role: "user", content: "Hello!" }], }); ``` ```bash theme={null} curl https://api.meshapi.ai/v1/chat/completions \ -H "Authorization: Bearer rsk_..." \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o", "messages": [{"role": "user", "content": "Hello!"}] }' ``` ## Next steps * [Quickstart](/docs/getting-started/quickstart) — make your first API call in under 5 minutes * [Account Configuration Checklist](/docs/getting-started/account-checklist) — a 15-minute setup pass over every setting that matters * [Authentication](/docs/getting-started/authentication) — understand key types and best practices * [Models & Pricing](https://meshapi.ai/models) — the live catalog with pricing * [Dashboard](/docs/getting-started/dashboard) — manage keys, billing, and usage analytics # Audio Source: https://developers.meshapi.ai/sdk/audio Text-to-speech synthesis, speech-to-text transcription, and listing available voices. ## Text to speech `audio.synthesize` returns raw audio bytes. ```python theme={null} from meshapi import MeshAPI, SpeechParams client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") audio_bytes = client.audio.synthesize( SpeechParams( input="Hello from MeshAPI audio test.", model="sarvam/bulbul:v3", ) ) # audio_bytes is a bytes object with open("output.wav", "wb") as f: f.write(audio_bytes) ``` ```javascript theme={null} const audio = await client.audio.synthesize({ input: "Hello from MeshAPI audio test.", model: "sarvam/bulbul:v3", }); // audio is a Uint8Array import { writeFileSync } from "fs"; writeFileSync("output.wav", audio); ``` ```go theme={null} model := "sarvam/bulbul:v3" audioBytes, err := client.Audio.Synthesize(ctx, meshapi.SpeechParams{ Input: "Hello from MeshAPI audio test.", Model: &model, }) if err != nil { log.Fatal(err) } os.WriteFile("output.wav", audioBytes, 0644) ``` ## Speech to text `audio.transcribe` accepts raw audio bytes plus a filename hint for format detection. **Argument order differs by language:** * Python: `transcribe(bytes, TranscriptionParams, filename=)` * Node.js: `transcribe(audio, { model }, { filename })` * Go: `Transcribe(ctx, bytes, filename, TranscriptionParams)` ```python theme={null} from meshapi import MeshAPI, SpeechParams, TranscriptionParams # Generate audio first, then transcribe it audio_bytes = client.audio.synthesize( SpeechParams(input="Hello from MeshAPI.", model="sarvam/bulbul:v3") ) result = client.audio.transcribe( audio_bytes, TranscriptionParams(model="sarvam/saaras:v3"), filename="tts_output.wav", ) print(result.text) ``` ```javascript theme={null} const audio = await client.audio.synthesize({ input: "Hello from MeshAPI.", model: "sarvam/bulbul:v3", }); const result = await client.audio.transcribe( audio, { model: "sarvam/saaras:v3" }, { filename: "tts_output.wav" }, ); console.log(result.text); ``` ```go theme={null} model := "sarvam/bulbul:v3" audioBytes, _ := client.Audio.Synthesize(ctx, meshapi.SpeechParams{ Input: "Hello from MeshAPI.", Model: &model, }) result, err := client.Audio.Transcribe(ctx, audioBytes, "tts_output.wav", meshapi.TranscriptionParams{ Model: "sarvam/saaras:v3", }) if err != nil { log.Fatal(err) } fmt.Println(result.Text) ``` ## Translate audio to English `POST /v1/audio/translations` transcribes audio in any language and returns the text **translated to English**. It returns the same `TranscriptionResponse` (with a `.text` field) as transcription. This is a distinct endpoint from the transcribe-and-translate helper (`POST /v1/audio/transcriptions/translate`). Use a speech model that supports translation — see the [Models](/sdk/models) list. `model` is required. ```python theme={null} from meshapi import MeshAPI, AudioTranslationsParams client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") with open("french_audio.mp3", "rb") as f: audio_bytes = f.read() result = client.audio.audio_translate( audio_bytes, AudioTranslationsParams(model="openai/whisper-large-v3"), filename="french_audio.mp3", ) print(result.text) # English translation ``` ```javascript theme={null} import { readFileSync } from "fs"; const audio = readFileSync("french_audio.mp3"); const result = await client.audio.translations( audio, { model: "openai/whisper-large-v3" }, { filename: "french_audio.mp3" }, ); console.log(result.text); // English translation ``` ```go theme={null} audioBytes, _ := os.ReadFile("french_audio.mp3") result, err := client.Audio.Translations(ctx, audioBytes, "french_audio.mp3", meshapi.AudioTranslationParams{ Model: "openai/whisper-large-v3", }) if err != nil { log.Fatal(err) } fmt.Println(result.Text) // English translation ``` Optional parameters: `prompt` (context hint), `response_format` (`json`, `text`, or `verbose_json`), and `temperature` (0–2). ## List voices ```python theme={null} from meshapi import MeshAPI, ListVoicesParams voices = client.audio.list_voices(ListVoicesParams(page_size=5)) ``` ```javascript theme={null} const voices = await client.audio.listVoices({ page_size: 5 }); ``` ```go theme={null} pageSize := 5 voices, err := client.Audio.ListVoices(ctx, &meshapi.ListVoicesParams{ PageSize: &pageSize, }) ``` ### `ListVoicesParams` fields | Field | Type | Notes | | - | - | - | | `page_size` | integer | Results per page | | `next_page_token` | string | Pagination cursor from the previous response | | `search` | string | Filter by name | | `voice_type` | string | `standard`, `cloned`, and so on | | `category` | string | Voice category filter | ## Get a voice Fetch a single voice by ID — `GET /v1/audio/voices/{voice_id}`. ```python theme={null} voice = client.audio.get_voice("voice-id") print(voice) ``` ```javascript theme={null} const voice = await client.audio.getVoice("voice-id"); console.log(voice); ``` ```go theme={null} voice, err := client.Audio.GetVoice(ctx, "voice-id") if err != nil { log.Fatal(err) } fmt.Println(voice) ``` ## Parameter reference ### `SpeechParams` (text to speech) | Field | Type | Notes | | - | - | - | | `input` | string | **Required.** Text to synthesize. | | `model` | string | **Required.** e.g. `sarvam/bulbul:v3` | | `voice` | string | Voice ID or name. Sarvam models ignore it; use `speaker` | | `speaker` | string | Sarvam models only: the voice, e.g. `ritu` | | `response_format` | string | Audio format, e.g. `wav`, `mp3`, `pcm_24000` | | `voice_settings` | object | Voice tuning, e.g. for ElevenLabs: `stability`, `similarity_boost`, `style`, `use_speaker_boost`, `speed` | | `pace` | number | Sarvam models only: speaking rate | ### `AudioTranslationsParams` (translate to English) | Field | Type | Notes | | - | - | - | | `model` | string | **Required.** Translation-capable model | | `prompt` | string | Context hint for the model | | `response_format` | string | `json`, `text`, or `verbose_json` | | `temperature` | number | 0–2 | # Batches Source: https://developers.meshapi.ai/sdk/batches Submit async batch jobs, check status, and cancel pending batches. The Batch API lets you submit many requests at once and retrieve results asynchronously — useful for large-scale processing where latency is not critical. Not all models support the Batch API. `openai/gpt-5-nano` is confirmed to support batching. ## Create a batch Pass an array of `requests`, each with a `custom_id` for correlating results and a `body` containing the chat completion parameters. ```python theme={null} from meshapi import MeshAPI, CreateBatchParams, BatchRequestItem client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") batch = client.batches.create( CreateBatchParams( requests=[ BatchRequestItem( custom_id="req-1", body={ "model": "openai/gpt-5-nano", "messages": [{"role": "user", "content": "Reply with the single word: hello"}], "max_tokens": 16, }, ), BatchRequestItem( custom_id="req-2", body={ "model": "openai/gpt-5-nano", "messages": [{"role": "user", "content": "Reply with the single word: world"}], "max_tokens": 16, }, ), ], metadata={"suite": "my-batch-job"}, ) ) print(batch.id) ``` ```javascript theme={null} const batch = await client.batches.create({ requests: [ { custom_id: "req-1", body: { model: "openai/gpt-5-nano", messages: [{ role: "user", content: "Reply with the single word: hello" }], max_tokens: 10, }, }, { custom_id: "req-2", body: { model: "openai/gpt-5-nano", messages: [{ role: "user", content: "Reply with the single word: world" }], max_tokens: 10, }, }, ], metadata: { suite: "my-batch-job" }, }); console.log(batch.id); ``` ```go theme={null} batch, err := client.Batches.Create(ctx, meshapi.CreateBatchParams{ Requests: []meshapi.BatchRequestItem{ { CustomID: "req-1", Body: map[string]interface{}{ "model": "openai/gpt-5-nano", "messages": []map[string]interface{}{{"role": "user", "content": "Reply with the single word: hello"}}, "max_tokens": 10, }, }, { CustomID: "req-2", Body: map[string]interface{}{ "model": "openai/gpt-5-nano", "messages": []map[string]interface{}{{"role": "user", "content": "Reply with the single word: world"}}, "max_tokens": 10, }, }, }, Metadata: map[string]interface{}{"suite": "go-batch-job"}, }) if err != nil { log.Fatal(err) } fmt.Println(batch.ID) ``` ## List batches ```python theme={null} batch_list = client.batches.list(limit=10) for item in batch_list.data: print(item.id, item.status) ``` ```javascript theme={null} const list = await client.batches.list({ limit: 10 }); list.data.forEach(b => console.log(b.id, b.status)); ``` ```go theme={null} limit := 10 listed, err := client.Batches.List(ctx, nil, &limit) for _, item := range listed.Data { fmt.Println(item.ID, item.Status) } ``` ## Get a batch ```python theme={null} got = client.batches.get(batch.id) print(got.id, got.status) ``` ```javascript theme={null} const got = await client.batches.get(batch.id); console.log(got.id, got.status); ``` ```go theme={null} got, err := client.Batches.Get(ctx, batch.ID) fmt.Println(got.ID, got.Status) ``` ## Cancel a batch ```python theme={null} cancelled = client.batches.cancel(batch.id) print(cancelled.id) ``` ```javascript theme={null} const cancelled = await client.batches.cancel(batch.id); console.log(cancelled.id); ``` ```go theme={null} cancelled, err := client.Batches.Cancel(ctx, batch.ID) fmt.Println(cancelled.ID, cancelled.Status) ``` # Chat Completions Source: https://developers.meshapi.ai/sdk/chat Send chat completion requests, build multi-turn conversations, and use prompt templates. ## Basic request ```python theme={null} from meshapi import MeshAPI, ChatCompletionParams, ChatMessage client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") resp = client.chat.completions.create( ChatCompletionParams( model="openai/gpt-4o-mini", messages=[ChatMessage(role="user", content="What is the capital of France? Reply in one word.")], max_tokens=10, temperature=0, ) ) print(resp.choices[0].message.content) ``` ```javascript theme={null} import { MeshAPI } from "meshapi-node-sdk"; const client = new MeshAPI({ baseUrl: "https://api.meshapi.ai", token: "rsk_..." }); const resp = await client.chat.completions.create({ model: "openai/gpt-4o-mini", messages: [{ role: "user", content: "What is 2 + 2? Reply in one word." }], max_tokens: 10, temperature: 0, }); console.log(resp.choices[0].message?.content); ``` ```go theme={null} import ( "context" meshapi "github.com/aifiesta/meshapi-go-sdk" ) model := "openai/gpt-4o-mini" maxTokens := 10 resp, err := client.Chat.Completions.Create(ctx, meshapi.ChatCompletionParams{ Model: &model, Messages: []meshapi.ChatMessage{{Role: "user", Content: "Reply with the single word: pong"}}, MaxTokens: &maxTokens, }) if err != nil { log.Fatal(err) } fmt.Println(*resp.Choices[0].Message.Content) ``` ## Multi-turn conversation Pass prior messages in the `messages` array to give the model conversation context. ```python theme={null} resp = client.chat.completions.create( ChatCompletionParams( model="openai/gpt-4o-mini", messages=[ ChatMessage(role="user", content="My favourite color is blue. Remember this."), ChatMessage(role="assistant", content="Got it! Your favourite color is blue."), ChatMessage(role="user", content="What is my favourite color? Reply in 3 words max."), ], max_tokens=20, temperature=0, ) ) print(resp.choices[0].message.content) ``` ```javascript theme={null} const resp = await client.chat.completions.create({ model: "openai/gpt-4o-mini", messages: [ { role: "user", content: "My favourite color is blue. Remember this." }, { role: "assistant", content: "Got it! Your favourite color is blue." }, { role: "user", content: "What is my favourite color? Reply in 3 words max." }, ], max_tokens: 20, temperature: 0, }); ``` ```go theme={null} model := "openai/gpt-4o-mini" maxTokens := 20 resp, err := client.Chat.Completions.Create(ctx, meshapi.ChatCompletionParams{ Model: &model, Messages: []meshapi.ChatMessage{ {Role: "system", Content: "You are a concise assistant. One sentence only."}, {Role: "user", Content: "What is the capital of France?"}, }, MaxTokens: &maxTokens, }) ``` ## Using a prompt template You can apply a saved [prompt template](/sdk/templates) to inject a system prompt and fill variables. ```python theme={null} resp = client.chat.completions.create( ChatCompletionParams( model="openai/gpt-4o-mini", messages=[ChatMessage(role="user", content="Introduce yourself.")], template="my-template-name", variables={"role": "friendly pirate"}, max_tokens=80, temperature=0, ) ) ``` ```javascript theme={null} const resp = await client.chat.completions.create({ model: "openai/gpt-4o-mini", messages: [{ role: "user", content: "Introduce yourself." }], template: "my-template-name", variables: { role: "friendly pirate" }, max_tokens: 80, temperature: 0, }); ``` ```go theme={null} template := "my-template-name" maxTokens := 80 resp, err := client.Chat.Completions.Create(ctx, meshapi.ChatCompletionParams{ Template: &template, Messages: []meshapi.ChatMessage{{Role: "user", Content: "Greet me"}}, MaxTokens: &maxTokens, }) ``` ## Async (Python) Use `AsyncMeshAPI` for async applications. ```python theme={null} import asyncio from meshapi import AsyncMeshAPI, ChatCompletionParams, ChatMessage async def main(): async with AsyncMeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") as client: resp = await client.chat.completions.create( ChatCompletionParams( model="openai/gpt-4o-mini", messages=[ChatMessage(role="user", content="Say hello.")], max_tokens=20, ) ) print(resp.choices[0].message.content) asyncio.run(main()) ``` ## Response fields | Field | Description | | - | - | | `resp.id` | Unique request ID | | `resp.model` | Model that served the request | | `resp.choices[0].message.content` | The assistant's reply | | `resp.choices[0].message.role` | Always `"assistant"` | | `resp.choices[0].finish_reason` | Usually `"stop"`, `"length"`, `"tool_calls"`, or `"content_filter"`. A provider stop reason with no OpenAI equivalent is passed through as-is | | `resp.usage` | Token counts: `prompt_tokens`, `completion_tokens`, `total_tokens` | ## Tool calling Pass a `tools` array to let the model call your own functions. The wire shape is OpenAI-compatible, so the same request works across all three SDKs — see [Tool Calling](/docs/capabilities/tool-calling) for the full request/response cycle. ```python theme={null} from meshapi import ChatCompletionParams, ChatMessage, Tool, ToolFunction params = ChatCompletionParams( model="openai/gpt-4o", messages=[ChatMessage(role="user", content="What is the weather in Paris?")], tools=[ Tool( type="function", function=ToolFunction( name="get_weather", description="Get current weather for a city", parameters={ "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, ), ) ], tool_choice="auto", ) for chunk in client.chat.completions.stream(params): delta = chunk.choices[0].delta if chunk.choices else None if delta and delta.tool_calls: print("tool call:", delta.tool_calls) elif delta and delta.content: print(delta.content, end="", flush=True) ``` ```javascript theme={null} const response = await client.chat.completions.create({ model: "openai/gpt-4o", messages: [{ role: "user", content: "What is the weather in Paris?" }], tools: [ { type: "function", function: { name: "get_weather", description: "Get current weather for a city", parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"], }, }, }, ], tool_choice: "auto", }); console.log(response.choices[0].message.tool_calls); ``` ```go theme={null} model := "openai/gpt-4o" description := "Get current weather for a city" response, err := client.Chat.Completions.Create(ctx, meshapi.ChatCompletionParams{ Model: &model, Messages: []meshapi.ChatMessage{{Role: "user", Content: "What is the weather in Paris?"}}, Tools: []meshapi.Tool{{ Type: "function", Function: meshapi.ToolFunction{ Name: "get_weather", Description: &description, Parameters: map[string]any{ "type": "object", "properties": map[string]any{"city": map[string]any{"type": "string"}}, "required": []string{"city"}, }, }, }}, ToolChoice: "auto", }) if err != nil { log.Fatal(err) } fmt.Println(response.Choices[0].Message.ToolCalls) ``` Any request carrying `tools` bypasses the [gateway response cache](/docs/capabilities/caching) — a tool call depends on live state, so replaying a stored one would be wrong. This is silent: no error, and no `X-Cache` header. # Model Compare Source: https://developers.meshapi.ai/sdk/compare Send the same prompt to multiple models in parallel and compare their responses. The Compare API fans out a single request to multiple models simultaneously and returns all responses together. This is useful for evaluating model quality side by side. ## Non-streaming compare ```python theme={null} from meshapi import MeshAPI, CompareParams, ChatMessage client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") result = client.compare.create( CompareParams( models=["openai/gpt-4o-mini", "anthropic/claude-haiku-4.5"], messages=[ChatMessage(role="user", content="Explain what an API is in one sentence.")], skip_comparison=True, max_tokens=100, ) ) for r in result.results: print(r.model, r.content) ``` ```javascript theme={null} const result = await client.compare.create({ models: ["openai/gpt-4o-mini", "anthropic/claude-haiku-4.5"], messages: [{ role: "user", content: "Explain what an API is in one sentence." }], skip_comparison: true, max_tokens: 100, }); result.results.forEach(r => { console.log(r.model, r.content); }); ``` ```go theme={null} maxTokens := 100 skip := true resp, err := client.Compare.Create(ctx, meshapi.CompareParams{ Models: []string{"openai/gpt-4o-mini", "anthropic/claude-haiku-4.5"}, Messages: []meshapi.ChatMessage{ {Role: "user", Content: "Explain what an API is in one sentence."}, }, MaxTokens: &maxTokens, SkipComparison: &skip, }) if err != nil { log.Fatal(err) } for _, r := range resp.Results { fmt.Println(r.Model, *r.Content) } ``` ## Streaming compare ```python theme={null} events = list( client.compare.stream( CompareParams( models=["openai/gpt-4o-mini", "anthropic/claude-haiku-4.5"], messages=[ChatMessage(role="user", content="Tell me a joke.")], skip_comparison=True, max_tokens=100, ) ) ) print(f"received {len(events)} events") ``` ```javascript theme={null} let events = 0; for await (const _event of client.compare.create({ models: ["openai/gpt-4o-mini", "anthropic/claude-haiku-4.5"], messages: [{ role: "user", content: "Tell me a joke." }], skip_comparison: true, max_tokens: 100, stream: true, })) { events++; } console.log(`received ${events} events`); ``` ```go theme={null} maxTokens := 100 skip := true eventCh, errCh := client.Compare.Stream(ctx, meshapi.CompareParams{ Models: []string{"openai/gpt-4o-mini", "anthropic/claude-haiku-4.5"}, Messages: []meshapi.ChatMessage{ {Role: "user", Content: "Tell me a joke."}, }, MaxTokens: &maxTokens, SkipComparison: &skip, }) count := 0 for range eventCh { count++ } if err := <-errCh; err != nil { log.Fatal(err) } fmt.Printf("received %d events\n", count) ``` ## Parameters | Parameter | Description | | - | - | | `models` | Array of model IDs to compare (2+) | | `messages` | Conversation messages sent to all models | | `skip_comparison` | Set to `true` to return raw results without a synthesis step | | `max_tokens` | Maximum tokens per model response | # Embeddings Source: https://developers.meshapi.ai/sdk/embeddings Generate vector embeddings for text using the embeddings endpoint. ## Generate embeddings ```python theme={null} from meshapi import MeshAPI, EmbeddingsParams client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") result = client.embeddings.create( EmbeddingsParams( model="openai/text-embedding-3-small", input="MeshAPI embeddings smoke test", ) ) print(result.model) # model used print(len(result.data)) # number of embedding items print(len(result.data[0].embedding)) # vector dimension ``` ```javascript theme={null} const result = await client.embeddings.create({ model: "openai/text-embedding-3-small", input: "MeshAPI embeddings smoke test", }); console.log(result.model); console.log(result.data[0].embedding.length); // vector dimension ``` ```go theme={null} model := "openai/text-embedding-3-small" resp, err := client.Embeddings.Create(ctx, meshapi.EmbeddingsParams{ Model: &model, Input: "MeshAPI embeddings smoke test", }) if err != nil { log.Fatal(err) } fmt.Printf("model=%s items=%d dims=%d\n", resp.Model, len(resp.Data), len(resp.Data[0].Embedding.Floats())) ``` ## Batch embeddings Pass a slice/array of strings to embed multiple inputs in a single request. ```python theme={null} result = client.embeddings.create( EmbeddingsParams( model="openai/text-embedding-3-small", input=["First document", "Second document", "Third document"], ) ) for item in result.data: print(f"index={item.index} dims={len(item.embedding)}") ``` ```javascript theme={null} const result = await client.embeddings.create({ model: "openai/text-embedding-3-small", input: ["First document", "Second document", "Third document"], }); result.data.forEach(item => { console.log(`index=${item.index} dims=${item.embedding.length}`); }); ``` ```go theme={null} model := "openai/text-embedding-3-small" resp, err := client.Embeddings.Create(ctx, meshapi.EmbeddingsParams{ Model: &model, Input: []string{"First document", "Second document", "Third document"}, }) for _, item := range resp.Data { fmt.Printf("index=%d dims=%d\n", item.Index, len(item.Embedding.Floats())) } ``` ## Response fields | Field | Description | | - | - | | `result.data` | Array of embedding objects | | `result.data[i].embedding` | Float vector | | `result.data[i].index` | Position in the input array | | `result.model` | Model used | | `result.usage` | Token counts | # Error Handling Source: https://developers.meshapi.ai/sdk/error-handling Catch typed API errors, read error codes, and configure automatic retries. All SDKs raise or throw a typed exception class when the API returns an error. The exception carries the HTTP status, a machine-readable error code, the request ID, and provider-specific details where applicable. ## Catching errors ```python theme={null} from meshapi import MeshAPI, MeshAPIError, ChatCompletionParams, ChatMessage client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") try: resp = client.chat.completions.create( ChatCompletionParams( model="openai/gpt-4o-mini", messages=[ChatMessage(role="user", content="hello")], ) ) except MeshAPIError as e: print(f"[{e.status}] {e.error_code}: {e}") print("Request ID:", e.request_id) if e.error_code == "rate_limit_exceeded": print(f"Retry after {e.retry_after_seconds}s") elif e.error_code == "spend_limit_exceeded": print("Out of balance or over a spend cap.") elif e.error_code == "unauthorized": print("Invalid API key.") ``` Mid-stream errors raise the same `MeshAPIError` from inside the iterator. ```javascript theme={null} import { MeshAPI, MeshAPIApiError } from "meshapi-node-sdk"; const client = new MeshAPI({ baseUrl: "https://api.meshapi.ai", token: "rsk_..." }); try { const resp = await client.chat.completions.create({ ... }); } catch (err) { if (err instanceof MeshAPIApiError) { console.error(`[${err.status}] ${err.errorCode}: ${err.message}`); console.error("Request ID:", err.requestId); switch (err.errorCode) { case "rate_limit_exceeded": console.log(`Retry after ${err.retryAfterSeconds}s`); break; case "spend_limit_exceeded": console.log("Out of balance or over a spend cap."); break; case "unauthorized": console.log("Invalid API key."); break; } } else { throw err; // Network error, AbortError, etc. } } ``` For streaming, mid-stream errors are also thrown as `MeshAPIApiError` inside the `for await` loop. ```go theme={null} import "errors" resp, err := client.Chat.Completions.Create(ctx, params) if err != nil { var svcErr *meshapi.MeshAPIError if errors.As(err, &svcErr) { fmt.Printf("[%d] %s: %s\n", svcErr.Status, svcErr.Code, svcErr.Message) fmt.Println("Request ID:", svcErr.RequestID) if svcErr.Code == "rate_limit_exceeded" && svcErr.RetryAfterSeconds != nil { fmt.Printf("Retry after %d seconds\n", *svcErr.RetryAfterSeconds) } } else { // Network or context error log.Fatal(err) } } ``` ## Error field names by language | Field | Python | Node.js | Go | | - | - | - | - | | HTTP status | `e.status` | `err.status` | `svcErr.Status` | | Error code | `e.error_code` | `err.errorCode` | `svcErr.Code` | | Message | `str(e)` | `err.message` | `svcErr.Message` | | Request ID | `e.request_id` | `err.requestId` | `svcErr.RequestID` | | Retry after | `e.retry_after_seconds` | `err.retryAfterSeconds` | `svcErr.RetryAfterSeconds` | ## Error codes | Code | HTTP | Meaning | | - | - | - | | `unauthorized` | 401 | Invalid or missing API key | | `forbidden` | 403 | Key suspended or access denied | | `not_found` / `model_not_found` | 404 | Resource or model not found | | `spend_limit_exceeded` | 402 | Insufficient balance, or a key or org spend cap reached | | `validation_error` | 422 | Request body failed validation | | `rate_limit_exceeded` | 429 | RPM or RPD limit hit | | `upstream_error` | 502/503 | Provider or server error | | `stream_interrupted` | — | Connection dropped mid-stream (Python, Go) | ## Automatic retries Non-streaming `create()` requests are retried automatically on `429`, `502`, `503`, and `504` with exponential backoff. The `Retry-After` header is respected on 429 responses. **Streams never retry automatically.** If a stream drops mid-response, the client raises an error and you need to restart the request manually. See [Streaming — Stream recovery](/sdk/streaming). ### Configuring retries (Python) ```python theme={null} client = MeshAPI( base_url="https://api.meshapi.ai", token="rsk_...", max_retries=5, # default: 3 — set to 0 to disable timeout=30.0, # seconds ) ``` # Image Generation Source: https://developers.meshapi.ai/sdk/images Generate images from text prompts and edit existing images. ## Generate an image ```python theme={null} import base64 from meshapi import MeshAPI, ImageGenerationParams client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") resp = client.images.generate( ImageGenerationParams( model="openai/gpt-image-1", prompt="A small blue square on a white background.", n=1, size="1024x1024", ) ) print(resp.created) # Unix timestamp print(len(resp.data)) # number of images # each item has either .b64_json or .url if resp.data[0].b64_json: image_bytes = base64.b64decode(resp.data[0].b64_json) elif resp.data[0].url: print(resp.data[0].url) ``` ```javascript theme={null} const resp = await client.images.generate({ model: "openai/gpt-image-1", prompt: "A small blue square on a white background.", n: 1, size: "1024x1024", }); console.log(resp.created); // Unix timestamp if (resp.data[0].b64_json) { const bytes = Buffer.from(resp.data[0].b64_json, "base64"); } else if (resp.data[0].url) { console.log(resp.data[0].url); } ``` ```go theme={null} model := "openai/gpt-image-1" n := 1 size := "1024x1024" resp, err := client.Images.Generate(ctx, meshapi.ImageGenerationParams{ Model: &model, Prompt: "A small blue square on a white background.", N: &n, Size: &size, }) if err != nil { log.Fatal(err) } fmt.Printf("created=%d images=%d\n", resp.Created, len(resp.Data)) if resp.Data[0].B64JSON != nil { // base64 image data } else if resp.Data[0].URL != nil { fmt.Println(*resp.Data[0].URL) } ``` ## Getting the image bytes Where the image data lands depends on the model and `response_format`. With the default (or `response_format="url"`), some models — including `openai/gpt-image-1` — return the image **inline as a `data:` URI in `url`** rather than in `b64_json`. Pass `response_format="b64_json"` to always get base64 in `b64_json`, or use the helper below to get raw bytes regardless of shape: ```python theme={null} # handles b64_json OR a data: URI in url image_bytes = resp.data[0].image_bytes() with open("out.png", "wb") as f: f.write(image_bytes) ``` ```go theme={null} imageBytes, err := resp.Data[0].Bytes() // handles B64JSON or a data: URI in URL if err != nil { log.Fatal(err) } os.WriteFile("out.png", imageBytes, 0644) ``` Both raise/return an error if `url` is a remote `http(s)` link (fetch it yourself). ## Edit an image `images.edit()` calls `POST /v1/images/edits` with a JSON body. Pass the source image (and an optional `mask` or `reference_images`) as a `data:image/...;base64,` URL or a bare base64 string; remote `http(s)` URLs are rejected with `422`. The response has the same shape as a generation. See [Image Generation: Edit an image](/docs/capabilities/image-generation#edit-an-image) for the operations each provider supports. ```python theme={null} import base64 from meshapi import ImageEditParams with open("cat.png", "rb") as f: image = "data:image/png;base64," + base64.b64encode(f.read()).decode() edited = client.images.edit( ImageEditParams(model="openai/gpt-image-1", image=image, prompt="Add a party hat on the cat") ) edited_bytes = edited.data[0].image_bytes() ``` ```javascript theme={null} import { readFileSync } from "node:fs"; const image = "data:image/png;base64," + readFileSync("cat.png").toString("base64"); const edited = await client.images.edit({ model: "openai/gpt-image-1", image, prompt: "Add a party hat on the cat", }); ``` ```go theme={null} raw, err := os.ReadFile("cat.png") if err != nil { log.Fatal(err) } image := "data:image/png;base64," + base64.StdEncoding.EncodeToString(raw) prompt := "Add a party hat on the cat" edited, err := client.Images.Edit(ctx, meshapi.ImageEditParams{ Model: "openai/gpt-image-1", Image: image, Prompt: &prompt, }) if err != nil { log.Fatal(err) } editedBytes, err := edited.Data[0].Bytes() if err != nil { log.Fatal(err) } os.WriteFile("cat-hat.png", editedBytes, 0644) ``` For streaming generation, `images.stream()` (`Images.Stream` in Go) sends `stream: true` and yields image chunks. See [Image Generation: Streaming](/docs/capabilities/image-generation#streaming). ## Response fields | Field | Description | | - | - | | `resp.created` | Unix timestamp of when the image was created | | `resp.data` | Array of image objects | | `resp.data[i].b64_json` | Base64-encoded image (if returned as base64) | | `resp.data[i].url` | Image URL — may be a hosted link **or** an inline `data:` URI | | `resp.data[i].image_bytes()` / `.Bytes()` | Raw image bytes from either shape (Python / Go) | `openai/gpt-image-1` is the recommended image generation model. Additional image generation models may be available depending on your account. # Models Source: https://developers.meshapi.ai/sdk/models List, filter, and search available models. ## List all models ```python theme={null} from meshapi import MeshAPI client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") models = client.models.list() for m in models: print(m.id, m.name, "free" if m.is_free else "paid") ``` ```javascript theme={null} const models = await client.models.list(); models.forEach(m => { console.log(m.id, m.name, m.is_free ? "free" : "paid"); }); ``` ```go theme={null} models, err := client.Models.List(ctx, meshapi.ListModelsParams{}) if err != nil { log.Fatal(err) } for _, m := range models { fmt.Printf("%s %s\n", m.ID, m.Name) } ``` ## List free or paid models Pass `free` to `list()`: `true` returns free models only, `false` paid models only. ```python theme={null} free = client.models.list(free=True) paid = client.models.list(free=False) for m in free: print(m.id) ``` ```javascript theme={null} const free = await client.models.list({ free: true }); const paid = await client.models.list({ free: false }); free.forEach(m => console.log(m.id)); ``` `client.models.free()` and `client.models.paid()` are shorthands for the same calls. ```go theme={null} freeTrue := true free, err := client.Models.List(ctx, meshapi.ListModelsParams{Free: &freeTrue}) if err != nil { log.Fatal(err) } freeFalse := false paid, err := client.Models.List(ctx, meshapi.ListModelsParams{Free: &freeFalse}) if err != nil { log.Fatal(err) } fmt.Printf("%d free, %d paid\n", len(free), len(paid)) ``` In the current Python (0.1.11) and Go (v0.1.12) releases, `client.models.free()` / `client.models.paid()` and `client.Models.Free()` / `client.Models.Paid()` call a path the API does not serve and fail with `404`. Use `list(free=True)` / `ListModelsParams{Free: ...}` as shown above. The Node.js shorthands work. ## Filter by type or provider `list()` also takes `type` (`text`, `embedding`, `image`, `audio`, `video`) and `provider` (a provider slug such as `openai` or `vertex`). Filters combine. The API also accepts `type=evaluation`, though the SDK type hints list only the five values above. ```python theme={null} image_models = client.models.list(type="image") openai_text = client.models.list(type="text", provider="openai") ``` ```javascript theme={null} const imageModels = await client.models.list({ type: "image" }); const openaiText = await client.models.list({ type: "text", provider: "openai" }); ``` ```go theme={null} modelType := "image" imageModels, err := client.Models.List(ctx, meshapi.ListModelsParams{Type: &modelType}) if err != nil { log.Fatal(err) } for _, m := range imageModels { fmt.Println(m.ID) } ``` ## Search models `search()` calls `GET /v1/models/search`: fuzzy text search (`q`) plus filters (`free`, `discounted`, `input_modality`, `output_modality`, `brand`), sorting (`sort`, `order`), and paging (`limit`, `offset`). It returns a page with `items`, `total`, `limit`, `offset`, and `brands`. ```python theme={null} from meshapi import ModelSearchParams page = client.models.search(ModelSearchParams(q="gemini", input_modality=["image"], limit=10)) print(page.total) for m in page.items: print(m.id) ``` ```javascript theme={null} const page = await client.models.search({ q: "gemini", input_modality: ["image"], limit: 10 }); console.log(page.total); page.items.forEach(m => console.log(m.id)); ``` ```go theme={null} q := "gemini" limit := 10 page, err := client.Models.Search(ctx, meshapi.ModelSearchParams{ Q: &q, InputModality: []string{"image"}, Limit: &limit, }) if err != nil { log.Fatal(err) } for _, m := range page.Items { fmt.Println(m.ID) } ``` ## Get one model ```python theme={null} m = client.models.get("openai/gpt-4o-mini") print(m.name, m.context_length) ``` ```javascript theme={null} const m = await client.models.get("openai/gpt-4o-mini"); console.log(m.name, m.context_length); ``` ```go theme={null} m, err := client.Models.Get(ctx, "openai/gpt-4o-mini") if err != nil { log.Fatal(err) } fmt.Println(m.Name) ``` ## Model fields | Field | Description | | - | - | | `id` | Model identifier used in requests (e.g. `openai/gpt-4o-mini`) | | `name` | Human-readable name | | `is_free` | Whether the model is available on the free tier | # SDK Reference Source: https://developers.meshapi.ai/sdk/overview Install the MeshAPI SDK for your language and make your first API call. MeshAPI provides official SDKs for Python, Node.js, and Go. Each SDK wraps the REST API with typed parameters, streaming helpers, and automatic retries. Java SDK is coming soon. ## Installation ```bash theme={null} pip install meshapi ``` For realtime (WebSocket) support: ```bash theme={null} pip install 'meshapi[realtime]' ``` Python 3.9+. Built on `httpx` and Pydantic v2. ```bash theme={null} npm install meshapi-node-sdk ``` Node 18+ is required. Node 18–21 requires `npm install ws` for realtime WebSocket support; Node 22+ has WebSocket built in. ```bash theme={null} go get github.com/aifiesta/meshapi-go-sdk ``` Go 1.21+. Java SDK — coming soon. ## Initialize the client ```python theme={null} from meshapi import MeshAPI client = MeshAPI( base_url="https://api.meshapi.ai", token="rsk_...", ) ``` For async applications, use `AsyncMeshAPI`: ```python theme={null} from meshapi import AsyncMeshAPI async with AsyncMeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") as client: ... ``` ```typescript theme={null} import { MeshAPI } from "meshapi-node-sdk"; const client = new MeshAPI({ baseUrl: "https://api.meshapi.ai", token: "rsk_...", }); ``` ```go theme={null} import meshapi "github.com/aifiesta/meshapi-go-sdk" client := meshapi.New(meshapi.Config{ BaseURL: "https://api.meshapi.ai", Token: "rsk_...", }) ``` ## API version The released SDKs send no `X-Mesh-Version` header, so each request is served at your key's pinned version, or the baseline `2026-08` if the key has none. A new key copies your organization's default version, and new organizations default to the newest version, `2026-09`. Check the key's **API Version** under **API Keys** in the dashboard before you pin it. See [API versioning](/docs/reference/api-versioning#pinning-a-whole-api-key). ## What's covered | Page | What it covers | | - | - | | [Chat Completions](/sdk/chat) | Basic chat, multi-turn, templates, async | | [Streaming](/sdk/streaming) | Streaming responses, cancellation, recovery | | [Structured Output](/sdk/structured-output) | JSON schema response format | | [Embeddings](/sdk/embeddings) | Single and batch embeddings | | [Image Generation](/sdk/images) | Generate and edit images | | [Audio](/sdk/audio) | Text-to-speech, speech-to-text, translate to English, list voices | | [Video Generation](/sdk/video) | Submit tasks, poll status, list | | [Batches](/sdk/batches) | Async batch job lifecycle | | [Model Compare](/sdk/compare) | Fan out to multiple models | | [Responses API](/sdk/responses) | Responses API create and stream | | [Prompt Templates](/sdk/templates) | Create, update, delete, use in chat | | [RAG (Files & Search)](/sdk/rag) | Upload, embed, and search documents | | [Realtime Audio](/sdk/realtime) | Bidirectional WebSocket audio sessions | | [Models](/sdk/models) | List, filter, search, and get models | | [Error Handling](/sdk/error-handling) | Typed exceptions, error codes, retries | The SDKs also wrap content moderation (`moderations.create`), web search (`web.search`), and Auto Router model selection (`router.select`). Their request and response fields match the [Moderations](/api/moderations/classify-content-against-the-content-policy), [Web search](/docs/capabilities/web-search), and [Router select](/api/router/router-select) references. # RAG (Files & Search) Source: https://developers.meshapi.ai/sdk/rag Upload documents, generate embeddings, and search with vector similarity. MeshAPI RAG lets you upload documents, embed them as vectors, and run semantic search queries against them. ## Full workflow The upload → embed → search lifecycle is 6 steps: Request a signed upload URL and get a `file_id`. Upload the file bytes directly to the signed URL using a plain HTTP PUT. Call `rag.get(file_id)` until `upload_status == "ready"`. Call `rag.embed([file_id])` to start the embedding process. Call `rag.get(file_id)` until `embedding_status == "ready"`. Run a semantic search query against the embedded file. ## Code examples ```python theme={null} import time import httpx from meshapi import MeshAPI from meshapi import InitUploadRequest, BulkEmbedRequest, SearchRequest client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") content = b"The quick brown fox jumps over the lazy dog." mime_type = "text/plain" # Step 1: Init upload upload = client.rag.init_upload( InitUploadRequest(file_name="my-doc.txt", mime_type=mime_type, embed=False) ) file_id = upload.file_id # Step 2: PUT to signed URL httpx.put(upload.signed_url, content=content, headers={"Content-Type": mime_type}).raise_for_status() # Step 3: Poll upload status deadline = time.monotonic() + 30 while time.monotonic() < deadline: status = client.rag.get(file_id) if status.upload_status == "ready": break time.sleep(2) # Step 4: Embed client.rag.embed(BulkEmbedRequest(file_ids=[file_id])) # Step 5: Poll embedding status deadline = time.monotonic() + 90 while time.monotonic() < deadline: status = client.rag.get(file_id) if status.embedding_status == "ready": break elif status.embedding_status == "failed": raise RuntimeError(f"embedding failed: {status.last_error_code}") time.sleep(3) # Step 6: Search results = client.rag.search( SearchRequest(query="fox jumps", top_k=5, file_ids=[file_id]) ) for r in results.results: print(f"score={r.score:.4f} text={r.text[:80]}") ``` ```javascript theme={null} const content = "The quick brown fox jumps over the lazy dog."; const mimeType = "text/plain"; // Step 1: Init upload const upload = await client.rag.initUpload({ file_name: "my-doc.txt", mime_type: mimeType, embed: false, }); const fileId = upload.file_id; // Step 2: PUT to signed URL await fetch(upload.signed_url, { method: "PUT", body: content, headers: { "Content-Type": mimeType }, }); // Step 3: Poll upload status const uploadDeadline = Date.now() + 30_000; while (Date.now() < uploadDeadline) { const s = await client.rag.get(fileId); if (s.upload_status === "ready") break; await new Promise(r => setTimeout(r, 2_000)); } // Step 4: Embed await client.rag.embed({ file_ids: [fileId] }); // Step 5: Poll embedding status const embedDeadline = Date.now() + 90_000; while (Date.now() < embedDeadline) { const s = await client.rag.get(fileId); if (s.embedding_status === "ready") break; if (s.embedding_status === "failed") throw new Error(`embedding failed: ${s.last_error_code}`); await new Promise(r => setTimeout(r, 3_000)); } // Step 6: Search const results = await client.rag.search({ query: "fox jumps", top_k: 5, file_ids: [fileId], }); results.results.forEach(r => console.log(`score=${r.score.toFixed(4)} text=${r.text.slice(0, 80)}`)); ``` ```go theme={null} content := []byte("The quick brown fox jumps over the lazy dog.") mimeType := "text/plain" // Step 1: Init upload embedFalse := false upload, err := client.RAG.InitUpload(ctx, meshapi.InitUploadRequest{ FileName: "my-doc.txt", MimeType: mimeType, Embed: &embedFalse, }) if err != nil { log.Fatal(err) } fileID := upload.FileID // Step 2: PUT to signed URL req, _ := http.NewRequest(http.MethodPut, upload.SignedURL, bytes.NewReader(content)) req.Header.Set("Content-Type", mimeType) http.DefaultClient.Do(req) // Step 3: Poll upload status deadline := time.Now().Add(30 * time.Second) for time.Now().Before(deadline) { s, _ := client.RAG.Get(ctx, fileID) if s.UploadStatus == "ready" { break } time.Sleep(2 * time.Second) } // Step 4: Embed client.RAG.Embed(ctx, meshapi.BulkEmbedRequest{FileIDs: []string{fileID}}) // Step 5: Poll embedding status deadline = time.Now().Add(90 * time.Second) for time.Now().Before(deadline) { s, _ := client.RAG.Get(ctx, fileID) if s.EmbeddingStatus == "ready" { break } time.Sleep(3 * time.Second) } // Step 6: Search topK := 5 results, err := client.RAG.Search(ctx, meshapi.SearchRequest{ Query: "fox jumps", TopK: &topK, FileIDs: []string{fileID}, }) for _, r := range results.Results { fmt.Printf("score=%.4f\n", r.Score) } ``` ## List files ```python theme={null} page = client.rag.list(limit=50, offset=0) print(f"total={page.total}") for f in page.files: print(f.file_id, f.embedding_status) ``` ```javascript theme={null} const page = await client.rag.list({ limit: 50, offset: 0 }); console.log(`total=${page.total}`); page.files.forEach(f => console.log(f.file_id, f.embedding_status)); ``` ```go theme={null} limit := 50 offset := 0 page, err := client.RAG.List(ctx, meshapi.ListRagFilesParams{ Limit: &limit, Offset: &offset, }) for _, f := range page.Files { fmt.Println(f.FileID, f.EmbeddingStatus) } ``` ## File statuses | Field | Values | | - | - | | `upload_status` | `pending` → `ready`, or `failed` | | `embedding_status` | `queued` → `processing` → `ready`, or `failed`. `pending` means the file was uploaded with `embed=False` and is waiting for `embed()` | ## Upload in one call `upload_file` (`uploadFile` in Node.js, `UploadFile` in Go) runs steps 1 and 2 for you: it calls `init_upload`, PUTs the bytes to the signed URL, and returns the same response with the `file_id`. If you leave `embed` unset, the file is embedded (and billed) as soon as the upload lands. The examples pass `embed=False` so you can call `embed()` yourself, as in the full workflow above. Drop it to embed right away. ```python theme={null} upload = client.rag.upload_file( file_name="my-doc.txt", mime_type="text/plain", content=b"The quick brown fox jumps over the lazy dog.", embed=False, ) print(upload.file_id) ``` ```javascript theme={null} const upload = await client.rag.uploadFile({ file_name: "my-doc.txt", mime_type: "text/plain", content: "The quick brown fox jumps over the lazy dog.", embed: false, }); console.log(upload.file_id); ``` ```go theme={null} embed := false upload, err := client.RAG.UploadFile(ctx, meshapi.UploadFileParams{ FileName: "my-doc.txt", MimeType: "text/plain", Content: []byte("The quick brown fox jumps over the lazy dog."), Embed: &embed, }) if err != nil { log.Fatal(err) } fmt.Println(upload.FileID) ``` ## Delete a file `DELETE /v1/files/{file_id}` removes the file's vectors and stored object, so it stops appearing in search. It returns `204`, and repeating it for an already-deleted file also returns `204`. The SDKs have no wrapper for it yet, so call it over HTTP with your `rsk_` key: ```bash theme={null} curl --fail-with-body -X DELETE https://api.meshapi.ai/v1/files/FILE_ID \ -H "Authorization: Bearer rsk_YOUR_KEY" ``` Treat the file as deleted only after a `204`. A `5xx` means the delete did not finish and the file may still show up in search, so repeat the call until it returns `204`. See [Delete a file](/docs/capabilities/rag#delete-a-file) for the full response table. ## Search options | Field | Type | Notes | | - | - | - | | `query` | string | Plain-language question | | `top_k` | integer | Results to return (1–50, default 5) | | `file_ids` | string\[] | Restrict the search to specific files | | `filter` | object | Match on metadata key-value pairs | | `date_from` | integer | Unix timestamp — only chunks created after this time | | `date_to` | integer | Unix timestamp — only chunks created before this time | ## RAG chat Combine search results with a chat completion to answer questions from your own documents. ```python theme={null} from meshapi import ChatCompletionParams, ChatMessage, SearchRequest results = client.rag.search(SearchRequest(query="What is the refund policy?", top_k=3)) context = "\n\n".join(r.text for r in results.results) reply = client.chat.completions.create( ChatCompletionParams( model="openai/gpt-4o-mini", messages=[ ChatMessage(role="system", content=f"Answer using only the context below.\n\n{context}"), ChatMessage(role="user", content="What is the refund policy?"), ], ) ) print(reply.choices[0].message.content) ``` ```javascript theme={null} const results = await client.rag.search({ query: "What is the refund policy?", top_k: 3 }); const context = results.results.map((r) => r.text).join("\n\n"); const reply = await client.chat.completions.create({ model: "openai/gpt-4o-mini", messages: [ { role: "system", content: `Answer using only the context below.\n\n${context}` }, { role: "user", content: "What is the refund policy?" }, ], }); console.log(reply.choices[0].message.content); ``` ```go theme={null} topK := 3 results, err := client.RAG.Search(ctx, meshapi.SearchRequest{ Query: "What is the refund policy?", TopK: &topK, }) if err != nil { log.Fatal(err) } var builder strings.Builder for _, r := range results.Results { builder.WriteString(r.Text + "\n\n") } model := "openai/gpt-4o-mini" reply, err := client.Chat.Completions.Create(ctx, meshapi.ChatCompletionParams{ Model: &model, Messages: []meshapi.ChatMessage{ {Role: "system", Content: "Answer using only the context below.\n\n" + builder.String()}, {Role: "user", Content: "What is the refund policy?"}, }, }) if err != nil { log.Fatal(err) } fmt.Println(*reply.Choices[0].Message.Content) ``` # Realtime Audio Source: https://developers.meshapi.ai/sdk/realtime Connect to a bidirectional WebSocket session for real-time audio and text. The Realtime API provides a bidirectional WebSocket session for low-latency audio streaming. The SDK wraps the raw WebSocket with typed send/receive helpers. **Python** requires `pip install 'meshapi[realtime]'` (adds `websockets>=12.0`).\ **Node.js** on Node 18–21 requires `npm install ws`. Node 22+ has WebSocket built in. **Protocol.** The realtime API uses OpenAI's GA event shape: configure the session with `session.type: "realtime"`, `output_modalities`, and an `audio` object (see below). Input audio is sent as base64 (`send_audio` handles this for you), and **output audio arrives as `response.output_audio.delta` events** — the SDK decodes these into `msg.audio` so you can play them directly. Audio is 24 kHz mono PCM16. ## Connect and configure ```python theme={null} from meshapi import MeshAPI client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") with client.realtime.connect(model="openai/gpt-realtime-mini") as session: session.send({ "type": "session.update", "session": { "type": "realtime", "output_modalities": ["audio"], # or ["text"] "instructions": "You are a helpful assistant.", "audio": { "input": {"format": {"type": "audio/pcm", "rate": 24000}}, "output": {"format": {"type": "audio/pcm", "rate": 24000}, "voice": "alloy"}, }, }, }) # session closes cleanly on context exit ``` Async variant: ```python theme={null} from meshapi import AsyncMeshAPI async with AsyncMeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") as client: async with client.realtime.connect(model="openai/gpt-realtime-mini") as session: await session.send({"type": "session.update", "session": {"type": "realtime", "output_modalities": ["text"]}}) ``` ```javascript theme={null} import { MeshAPI } from "meshapi-node-sdk"; const client = new MeshAPI({ baseUrl: "https://api.meshapi.ai", token: "rsk_..." }); const session = await client.realtime.connect({ model: "openai/gpt-realtime-mini" }); await session.send({ type: "session.update", session: { type: "realtime", output_modalities: ["audio"], instructions: "You are a helpful assistant.", audio: { input: { format: { type: "audio/pcm", rate: 24000 } }, output: { format: { type: "audio/pcm", rate: 24000 }, voice: "alloy" }, }, }, }); ``` ```go theme={null} session, err := client.Realtime.Connect(ctx, meshapi.RealtimeConnectParams{ Model: "openai/gpt-realtime-mini", }) if err != nil { log.Fatal(err) } defer session.Close() session.Send(ctx, map[string]any{ "type": "session.update", "session": map[string]any{ "type": "realtime", "output_modalities": []string{"audio"}, "instructions": "You are a helpful assistant.", "audio": map[string]any{ "input": map[string]any{"format": map[string]any{"type": "audio/pcm", "rate": 24000}}, "output": map[string]any{"format": map[string]any{"type": "audio/pcm", "rate": 24000}, "voice": "alloy"}, }, }, }) ``` ## Receive frames The server sends a `session.created` event immediately after the WebSocket handshake completes. ```python theme={null} with client.realtime.connect(model="openai/gpt-realtime-mini") as session: # Receive a single frame msg = session.receive() if msg.audio: # set for output_audio.delta events play_audio(msg.audio) elif msg.event: print("event type:", msg.event.get("type")) # Iterate over frames for msg in session: if msg.audio: play_audio(msg.audio) # 24kHz mono PCM16 elif msg.event and msg.event.get("type") == "response.output_text.delta": print(msg.event.get("delta"), end="", flush=True) elif msg.event and msg.event.get("type") == "response.done": break ``` ```javascript theme={null} // Async iterator for await (const msg of session) { if (msg.audio) { playAudio(msg.audio); // Uint8Array } else if (msg.event?.type === "response.output_text.delta") { process.stdout.write(msg.event.delta); } else if (msg.event?.type === "response.done") { break; } } await session.close(); // Event emitter style session .on("message", (msg) => { if (msg.audio) playAudio(msg.audio); }) .on("error", (err) => console.error(err)) .on("close", (code) => console.log("closed:", code)); ``` ```go theme={null} // Single receive (blocking) msg, err := session.Receive(ctx) if msg.Audio != nil { playAudio(msg.Audio) } else if msg.Event != nil { fmt.Println("event:", msg.Event["type"]) } // Channel-based concurrent pump msgCh, errCh := session.Events(ctx) for msg := range msgCh { if msg.Audio != nil { playAudio(msg.Audio) } else if msg.Event != nil { switch msg.Event["type"] { case "response.output_text.delta": fmt.Print(msg.Event["delta"]) case "response.done": fmt.Println("\n[done]") } } } if err := <-errCh; err != nil { log.Fatal(err) } ``` ## Send audio Pass raw 16-bit PCM audio (24kHz mono) to `send_audio` / `sendAudio` / `SendAudio`. ```python theme={null} session.send_audio(pcm_bytes) session.send({"type": "input_audio_buffer.commit"}) session.send({"type": "response.create"}) ``` ```javascript theme={null} await session.sendAudio(pcmBytes); // Uint8Array await session.send({ type: "input_audio_buffer.commit" }); await session.send({ type: "response.create" }); ``` ```go theme={null} session.SendAudio(ctx, pcmBytes) session.Send(ctx, map[string]any{"type": "input_audio_buffer.commit"}) session.Send(ctx, map[string]any{"type": "response.create"}) ``` ## Error handling Errors in the WebSocket session are surfaced as `RealtimeError` (`*meshapi.RealtimeError` in Go). ```python theme={null} from meshapi.resources.realtime import RealtimeError try: msg = session.receive() except RealtimeError as e: print("code:", e.code) print("message:", str(e)) ``` ```javascript theme={null} import { RealtimeError } from "meshapi-node-sdk"; try { for await (const msg of session) { ... } } catch (err) { if (err instanceof RealtimeError) { console.error("code:", err.code); console.error("message:", err.message); } } ``` ```go theme={null} msg, err := session.Receive(ctx) if err != nil { var re *meshapi.RealtimeError if errors.As(err, &re) { fmt.Println("code:", re.Code) } } ``` ## Supported models | Model ID | Mode | | - | - | | `openai/gpt-realtime-2.1` | Speech-to-speech | | `openai/gpt-realtime-2.1-mini` | Speech-to-speech | | `openai/gpt-realtime-2` | Speech-to-speech | | `openai/gpt-realtime-1.5` | Speech-to-speech | | `openai/gpt-realtime` | Speech-to-speech | | `openai/gpt-realtime-mini` | Speech-to-speech | | `openai/gpt-realtime-translate` | Speech-to-speech translation | | `elevenlabs/scribe_v2_realtime` | Realtime speech-to-text | Consult `GET /v1/models` for the current set of realtime-capable models — the catalog is live and this table is a snapshot. # Responses API Source: https://developers.meshapi.ai/sdk/responses Use the Responses API to create and stream model responses. The Responses API is an alternative to the Chat Completions API with a different request shape — it takes an `input` string instead of a `messages` array. ## Create a response ```python theme={null} from meshapi import MeshAPI, ResponsesParams client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") resp = client.responses.create( ResponsesParams( model="openai/gpt-4o-mini", input="Reply with exactly the word: ok", max_output_tokens=16, ) ) print(resp.id) print(resp.model) ``` ```javascript theme={null} const resp = await client.responses.create({ model: "openai/gpt-4o-mini", input: "Reply with exactly the word: ok", max_output_tokens: 16, }); console.log(resp.id); console.log(resp.status); ``` ```go theme={null} maxTokens := 16 model := "openai/gpt-4o-mini" resp, err := client.Responses.Create(ctx, meshapi.ResponsesParams{ Model: &model, Input: "Reply with exactly the word: ok", MaxOutputTokens: &maxTokens, }) if err != nil { log.Fatal(err) } if resp.ID != nil { fmt.Println("id:", *resp.ID) } if resp.Status != nil { fmt.Println("status:", *resp.Status) } ``` ## Stream a response ```python theme={null} from meshapi import MeshAPIError try: events = list( client.responses.stream( ResponsesParams( model="openai/gpt-4o-mini", input="Count from 1 to 3.", max_output_tokens=32, ) ) ) print(f"received {len(events)} events") except MeshAPIError as e: if e.status == 501: print("Streaming not supported for this model") else: raise ``` ```javascript theme={null} let events = 0; for await (const _event of client.responses.create({ model: "openai/gpt-4o-mini", input: "Count from 1 to 3.", max_output_tokens: 20, stream: true, })) { events++; } console.log(`received ${events} events`); ``` ```go theme={null} maxTokens := 32 model := "openai/gpt-4o-mini" eventCh, errCh := client.Responses.Stream(ctx, meshapi.ResponsesParams{ Model: &model, Input: "Count from 1 to 3.", MaxOutputTokens: &maxTokens, }) count := 0 for range eventCh { count++ } if err := <-errCh; err != nil { log.Fatal(err) } fmt.Printf("received %d events\n", count) ``` ## Parameters | Parameter | Description | | - | - | | `model` | Model ID | | `input` | The input text prompt | | `max_output_tokens` | Maximum tokens in the response | ## Retrieve background responses Use `list` and `get` for persisted/background response jobs. A response returned from a synchronous `create` call is not guaranteed to be retrievable later by ID. The SDKs expose both as `client.responses.list()` and `client.responses.get(id)` in Python and Node.js, and `client.Responses.List` and `client.Responses.Get` in Go. # Streaming Source: https://developers.meshapi.ai/sdk/streaming Stream chat completions token by token, cancel mid-stream, and handle stream errors. ## Basic streaming The streaming API differs by language: Python uses a separate `stream()` method, Node.js passes `stream: true` to `create()`, and Go returns channels. `stream()` is a separate method from `create()`. It returns a sync iterator. ```python theme={null} from meshapi import MeshAPI, ChatCompletionParams, ChatMessage client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") for chunk in client.chat.completions.stream( ChatCompletionParams( model="openai/gpt-4o-mini", messages=[ChatMessage(role="user", content="Count exactly from 1 to 3.")], ) ): if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.content is not None: print(chunk.choices[0].delta.content, end="", flush=True) ``` Pass `stream: true` to the same `create()` method. It returns an `AsyncIterable` of chunks. ```javascript theme={null} for await (const chunk of client.chat.completions.create({ model: "openai/gpt-4o-mini", messages: [{ role: "user", content: "Count exactly from 1 to 3." }], stream: true, })) { process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); } ``` `Stream()` returns two channels — one for chunks, one for the terminal error. ```go theme={null} model := "openai/gpt-4o-mini" maxTokens := 40 chunkCh, errCh := client.Chat.Completions.Stream(ctx, meshapi.ChatCompletionParams{ Model: &model, Messages: []meshapi.ChatMessage{{Role: "user", Content: "Count from 1 to 5."}}, MaxTokens: &maxTokens, }) for chunk := range chunkCh { if len(chunk.Choices) > 0 && chunk.Choices[0].Delta != nil && chunk.Choices[0].Delta.Content != nil { fmt.Print(*chunk.Choices[0].Delta.Content) } } if err := <-errCh; err != nil { log.Fatal(err) } ``` ## Async streaming (Python) Use `AsyncMeshAPI` to stream in async contexts. ```python theme={null} import asyncio from meshapi import AsyncMeshAPI, ChatCompletionParams, ChatMessage async def main(): async with AsyncMeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") as client: async for chunk in client.chat.completions.stream( ChatCompletionParams( model="openai/gpt-4o-mini", messages=[ChatMessage(role="user", content="Explain async/await.")], ) ): if chunk.choices and chunk.choices[0].delta: print(chunk.choices[0].delta.content or "", end="", flush=True) asyncio.run(main()) ``` ## Cancelling a stream Break out of the iterator at any point — no cleanup needed. ```python theme={null} for chunk in client.chat.completions.stream(params): print(chunk.choices[0].delta.content or "", end="") if should_stop(): break ``` Pass an `AbortSignal` to cancel the request mid-stream. ```javascript theme={null} const controller = new AbortController(); setTimeout(() => controller.abort(), 5_000); // cancel after 5s try { for await (const chunk of client.chat.completions.create( { model: "openai/gpt-4o-mini", messages: [...], stream: true }, { signal: controller.signal }, )) { process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); } } catch (err) { if (err.name === "AbortError") { console.log("Stream cancelled."); } } ``` Cancel the context to stop the stream. ```go theme={null} ctx, cancel := context.WithCancel(context.Background()) defer cancel() chunkCh, errCh := client.Chat.Completions.Stream(ctx, params) received := 0 for range chunkCh { received++ if received >= 3 { cancel() // stop the stream break } } for range chunkCh {} // drain <-errCh // may be context.Canceled or nil — both acceptable ``` ## Stream recovery (Python) Streams do not automatically retry mid-stream. If the connection drops, a `MeshAPIError` with `error_code="stream_interrupted"` is raised. Catch it and restart: ```python theme={null} from meshapi import MeshAPIError try: for chunk in client.chat.completions.stream(params): process(chunk) except MeshAPIError as e: if e.error_code == "stream_interrupted": # restart from scratch — no partial resume for chunk in client.chat.completions.stream(params): process(chunk) ``` Non-streaming `create()` requests retry automatically on `429`, `502`, `503`, and `504`. Streams never retry automatically — the client would have to re-generate content already received. # Structured Output Source: https://developers.meshapi.ai/sdk/structured-output Constrain model output to a JSON schema for reliable structured data extraction. Pass a `response_format` with `type: "json_schema"` to get a response that always matches your schema. Works with any model that supports `response_format`, including OpenAI and Google Gemini models (e.g. `google/gemini-2.5-flash`). **Not every model enforces `response_format`.** It only takes effect on models that support structured output. If a model doesn't, the request still **succeeds and returns ordinary text** — it does not error. Always parse/validate the response, and prefer a model with first-class support such as OpenAI models or Google Gemini (e.g. `google/gemini-2.5-flash`). ## Output modes `response_format` is an object whose `type` selects how the output is shaped: | `type` | Behavior | | - | - | | `"text"` | Default. Free-form text — identical to omitting `response_format`. | | `"json_object"` | The model returns syntactically valid JSON. Describe the keys you want in your prompt; the structure is not otherwise enforced. | | `"json_schema"` | The model returns JSON that conforms to the JSON Schema you supply. Field names and types are constrained regardless of the prompt. | In the JSON modes the content comes back as a **string** in `choices[0].message.content` — parse it client-side. ## Basic example ```python theme={null} import json from meshapi import MeshAPI, ChatCompletionParams, ChatMessage client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") schema = { "type": "json_schema", "json_schema": { "name": "country_info", "schema": { "type": "object", "properties": { "capital": {"type": "string"}, "country": {"type": "string"}, }, "required": ["capital", "country"], "additionalProperties": False, }, }, } resp = client.chat.completions.create( ChatCompletionParams( model="google/gemini-2.5-flash", messages=[ChatMessage(role="user", content="What is the capital of France? Use the provided schema.")], response_format=schema, max_tokens=1000, temperature=0, ) ) data = json.loads(resp.choices[0].message.content) print(data["capital"]) # "Paris" print(data["country"]) # "France" ``` ```javascript theme={null} const schema = { type: "json_schema", json_schema: { name: "country_info", schema: { type: "object", properties: { capital: { type: "string" }, country: { type: "string" }, }, required: ["capital", "country"], additionalProperties: false, }, }, }; const resp = await client.chat.completions.create({ model: "google/gemini-2.5-flash", messages: [{ role: "user", content: "What is the capital of France? Use the provided schema." }], response_format: schema, max_tokens: 1000, temperature: 0, }); const data = JSON.parse(resp.choices[0].message?.content ?? "{}"); console.log(data.capital); // "Paris" ``` ```go theme={null} schema := map[string]interface{}{ "type": "json_schema", "json_schema": map[string]interface{}{ "name": "country_info", "schema": map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "capital": map[string]interface{}{"type": "string"}, "country": map[string]interface{}{"type": "string"}, }, "required": []string{"capital", "country"}, "additionalProperties": false, }, }, } model := "google/gemini-2.5-flash" maxTokens := 1000 resp, err := client.Chat.Completions.Create(ctx, meshapi.ChatCompletionParams{ Model: &model, Messages: []meshapi.ChatMessage{ {Role: "user", Content: "What is the capital of France? Use the provided schema."}, }, ResponseFormat: schema, MaxTokens: &maxTokens, }) if err != nil { log.Fatal(err) } var data map[string]interface{} json.Unmarshal([]byte(*resp.Choices[0].Message.Content), &data) fmt.Println(data["capital"]) // "Paris" ``` ## Valid JSON without a schema When you only need parseable JSON and don't want to define a schema, use `json_object` and describe the expected keys in the prompt: ```python theme={null} resp = client.chat.completions.create( ChatCompletionParams( model="google/gemini-2.5-flash", messages=[ ChatMessage(role="system", content="Respond only with JSON."), ChatMessage(role="user", content="List three fruits and their colors with keys name and color."), ], response_format={"type": "json_object"}, ) ) data = json.loads(resp.choices[0].message.content) ``` The output is always valid JSON, but the exact keys depend on the model following your prompt. Use `json_schema` when you need that guarantee. ## How it works across providers `response_format` follows the OpenAI convention and is forwarded to the upstream provider. For providers with a different native contract, Mesh API translates it automatically — for example, Google Gemini models on Vertex AI are converted to Gemini's native structured-output config (`responseMimeType` for `json_object`, plus `responseSchema` for `json_schema`), so enforcement runs on the provider side. ## Supported models Structured output works with any model that supports `response_format` — this includes OpenAI and Google Gemini models (e.g. `google/gemini-2.5-flash`). Models that don't support it simply return ordinary text. Use `GET /v1/models` to see the models enabled on your account. ## Notes * Set `additionalProperties: false` to prevent extra fields in the response. * `finish_reason` will be `"stop"` on success. * The response content is a JSON string — parse it with `json.loads()` / `JSON.parse()` / `json.Unmarshal`. ## Typed parsing with `parse()` (Node.js) The Node.js SDK (2.0.0) has `client.chat.completions.parse()`: it builds `response_format` from a schema, sends a non-streaming request, and returns the parsed object. Pass a [Standard Schema](https://standardschema.dev) validator (Zod v4, Valibot with `@valibot/to-json-schema`, ArkType) to get a validated, typed result, or a raw JSON schema to get `JSON.parse` output without validation. ```javascript theme={null} import { z } from "zod"; import { StructuredOutputError } from "meshapi-node-sdk"; const Country = z.object({ capital: z.string(), country: z.string() }); try { const result = await client.chat.completions.parse( { model: "google/gemini-2.5-flash", messages: [{ role: "user", content: "What is the capital of France?" }], }, Country, { maxRetries: 2 }, ); console.log(result.capital); // "Paris" } catch (err) { if (err instanceof StructuredOutputError) console.error("Still invalid after retries:", err.message); else throw err; } ``` `maxRetries` feeds a response that fails validation back to the model with the error appended. Each retry is a billed call; the default is `0`. An empty response (a refusal or a tool call) is never retried. If the response still can't be parsed, `parse()` throws `StructuredOutputError`. The usual cause is a model that doesn't support structured output. `parse()` is not in the current Python (0.1.11) or Go (v0.1.12) releases. In those SDKs, call `create()` with `response_format` and parse `choices[0].message.content` yourself, as in the examples above. # Prompt Templates Source: https://developers.meshapi.ai/sdk/templates Create, update, delete, and use prompt templates with variable substitution. Prompt templates let you save reusable system prompts with `{{variable}}` placeholders. Pass the template name in a chat request to apply it. ## Create a template ```python theme={null} from meshapi import MeshAPI from meshapi import CreateTemplateParams client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") tmpl = client.templates.create( CreateTemplateParams( name="my-assistant", description="A friendly pirate assistant", system="You are a {{role}}. Always reply in exactly one sentence.", variables=["role"], ) ) print(tmpl.id) print(tmpl.name) ``` ```javascript theme={null} const tmpl = await client.templates.create({ name: "my-assistant", description: "A friendly pirate assistant", system: "You are a {{role}}. Always reply in exactly one sentence.", variables: ["role"], }); console.log(tmpl.id); console.log(tmpl.name); ``` ```go theme={null} desc := "A friendly pirate assistant" system := "You are a {{role}}. Always reply in exactly one sentence." tmpl, err := client.Templates.Create(ctx, meshapi.CreateTemplateParams{ Name: "my-assistant", Description: &desc, System: &system, }) if err != nil { log.Fatal(err) } fmt.Println(tmpl.ID, tmpl.Name) ``` ## List templates ```python theme={null} templates = client.templates.list() for t in templates: print(t.id, t.name) ``` ```javascript theme={null} const templates = await client.templates.list(); templates.forEach(t => console.log(t.id, t.name)); ``` ```go theme={null} all, err := client.Templates.List(ctx) for _, t := range all { fmt.Println(t.ID, t.Name) } ``` ## Get a template ```python theme={null} got = client.templates.get(tmpl.id) print(got.name) ``` ```javascript theme={null} const got = await client.templates.get(tmpl.id); console.log(got.name); ``` ```go theme={null} got, err := client.Templates.Get(ctx, tmpl.ID) fmt.Println(got.Name) ``` ## Update a template ```python theme={null} from meshapi import UpdateTemplateParams updated = client.templates.update( tmpl.id, UpdateTemplateParams(description="Updated description"), ) print(updated.description) ``` ```javascript theme={null} const updated = await client.templates.update(tmpl.id, { description: "Updated description", }); console.log(updated.description); ``` ```go theme={null} newDesc := "Updated description" updated, err := client.Templates.Update(ctx, tmpl.ID, meshapi.UpdateTemplateParams{ Description: &newDesc, }) fmt.Println(*updated.Description) ``` ## Delete a template ```python theme={null} client.templates.delete(tmpl.id) ``` ```javascript theme={null} await client.templates.delete(tmpl.id); ``` ```go theme={null} err := client.Templates.Delete(ctx, tmpl.ID) ``` ## Use a template in a chat request Pass the template name and variable values in the chat request. See [Chat Completions — Using a prompt template](/sdk/chat) for the full example. ```python theme={null} from meshapi import ChatCompletionParams, ChatMessage resp = client.chat.completions.create( ChatCompletionParams( model="openai/gpt-4o-mini", messages=[ChatMessage(role="user", content="Introduce yourself.")], template=tmpl.name, variables={"role": "friendly pirate"}, max_tokens=80, ) ) print(resp.choices[0].message.content) ``` ```javascript theme={null} const resp = await client.chat.completions.create({ model: "openai/gpt-4o-mini", messages: [{ role: "user", content: "Introduce yourself." }], template: tmpl.name, variables: { role: "friendly pirate" }, max_tokens: 80, }); ``` ```go theme={null} maxTokens := 80 resp, err := client.Chat.Completions.Create(ctx, meshapi.ChatCompletionParams{ Template: &tmpl.Name, Messages: []meshapi.ChatMessage{{Role: "user", Content: "Introduce yourself."}}, Variables: map[string]string{"role": "friendly pirate"}, MaxTokens: &maxTokens, }) ``` # Video Generation Source: https://developers.meshapi.ai/sdk/video Submit video generation tasks, poll for completion, and list past generations. Video generation is asynchronous — you submit a task and poll for its status until it completes. ## Generate a video ```python theme={null} from meshapi import MeshAPI, VideoGenerationParams, VideoContentItem client = MeshAPI(base_url="https://api.meshapi.ai", token="rsk_...") resp = client.videos.generate( VideoGenerationParams( model="byteplus/dreamina-seedance-2-0", content=[VideoContentItem(type="text", text="A serene mountain lake at sunrise")], ) ) print(resp.id) # task ID ``` ```javascript theme={null} const resp = await client.videos.generate({ model: "byteplus/dreamina-seedance-2-0", content: [{ type: "text", text: "A serene mountain lake at sunrise" }], }); console.log(resp.id); // task ID ``` ```go theme={null} text := "A serene mountain lake at sunrise" resp, err := client.Videos.Generate(ctx, meshapi.VideoGenerationParams{ Model: "byteplus/dreamina-seedance-2-0", Content: []meshapi.VideoContentItem{ {Type: "text", Text: &text}, }, }) if err != nil { log.Fatal(err) } fmt.Printf("task_id=%s\n", resp.ID) ``` ## Retrieve task status ```python theme={null} task = client.videos.retrieve(resp.id) print(task.id) print(task.status) # queued / running / succeeded / failed / expired / cancelled ``` ```javascript theme={null} const task = await client.videos.retrieve(resp.id); console.log(task.id); console.log(task.status); // queued / running / succeeded / failed / expired / cancelled ``` ```go theme={null} task, err := client.Videos.Retrieve(ctx, resp.ID) fmt.Printf("status=%s\n", task.Status) ``` ## Poll until complete ```python theme={null} import time while True: task = client.videos.retrieve(resp.id) if task.status == "succeeded": print("done:", task.id) break elif task.status in ("failed", "expired", "cancelled"): raise RuntimeError(f"video generation {task.status}: {task.id}") time.sleep(5) ``` ## List past generations ```python theme={null} from meshapi import ListVideoGenerationsParams listing = client.videos.list(ListVideoGenerationsParams(limit=5)) print(f"total={listing.total} items={len(listing.data)}") for item in listing.data: print(item.id, item.status) ``` ```javascript theme={null} const listing = await client.videos.list({ limit: 5 }); console.log(`total=${listing.total} items=${listing.data.length}`); ``` ```go theme={null} limit := 5 listing, err := client.Videos.List(ctx, &meshapi.ListVideoGenerationsParams{ Limit: &limit, }) fmt.Printf("total=%d items=%d\n", listing.Total, len(listing.Data)) ``` ## Task statuses | Status | Meaning | | - | - | | `queued` | Task accepted, not yet started | | `running` | Generation in progress | | `succeeded` | Generation complete | | `failed` | Generation failed | | `expired` | Ran out of time before finishing | | `cancelled` | The task was cancelled | `succeeded`, `failed`, `expired`, and `cancelled` are terminal. Stop polling on any of them.