Docs
The API serves MASHBOX Creature and Maya1 text to speech to approved companies. Once approved, you create a key and call either one over HTTP.
Quickstart
- Request access: fill in the business access form. Access is free for approved businesses.
- Once approved, create a key on the keys page. It's shown once, so copy it.
- Send a request with the key in the
Authorizationheader, and read the result: the audio file, or a job you poll until it's done.
export MASHBOX_API_KEY="your key"
curl https://inference.mashbox.gg/v1/models -H "Authorization: Bearer $MASHBOX_API_KEY"Calling the API
- HTTP with JSON bodies, from any language. Base URL:
https://inference.mashbox.gg/v1 - OpenAI SDK. Text to speech takes the OpenAI speech request. Set the base URL to
https://inference.mashbox.gg/v1and use your MASHBOX key. See OpenAI compatibility.
Authentication
Send Authorization: Bearer $MASHBOX_API_KEY on every request. Create and manage keys on the keys page; a new key is shown once. Keys belong on servers: a request that carries a browser Origin header is refused with origin_not_allowed.
GET /v1/key describes the key you call it with.
MASHBOX Creature
Pipeline Experimental For approved accounts
Experimental: output quality is still improving; results may need cleanup.
One text prompt in; a textured, rigged and animated 3D creature out, as a GLB file. POST /v1/creatures with model mbx-creature answers 202 at once with a job: its id and status. The job also gives position, its place in the queue, eta_s, the estimated seconds until it is done, progress, how far along it is and stage, the step it is on. Poll the job until it is completed, then download the files. A creature takes minutes, not seconds. You can also try it in the playground.
Each request runs these steps in order:
- A concept image from your prompt, made with Z-Image Turbo.
- A 3D mesh built from that image.
- Texture painted onto the mesh.
- A skeleton and skin weights fitted to the mesh.
- Motion clips for that skeleton, from our own motion model.
- Everything packed into one GLB file for your game.
Request fields
| Field | Type | Required | About |
|---|---|---|---|
body | string | no | Body type. auto picks for you; humanoid builds a two-legged figure. metadata.json says which rig route ran. One of auto, creature, humanoid. Default auto. |
model | string | no | The model id, mbx-creature. |
motions | array | no | One animation clip per entry, described in plain words. Default: walk, trot, walk and turn left, leap, pounce, die. |
prompt | string | yes | What to make. Up to 200 characters. |
seed | integer | no | Same seed + same input = same output. From 0, to 2,147,483,647. Default 0. |
style | string | no | Art style. Leave it out for the default realistic look. One of anime, cartoon, storybook, horror. |
curl https://inference.mashbox.gg/v1/creatures \
-H "Authorization: Bearer $MASHBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "mbx-creature", "prompt": "a six-legged desert lizard with a spiked tail"}'
# then, every 2 seconds until status is completed or failed:
curl https://inference.mashbox.gg/v1/jobs/$JOB_ID -H "Authorization: Bearer $MASHBOX_API_KEY"import os, time, requests
H = {"Authorization": "Bearer " + os.environ["MASHBOX_API_KEY"]}
job = requests.post("https://inference.mashbox.gg/v1/creatures", headers=H, json={"model": "mbx-creature", "prompt": "a six-legged desert lizard with a spiked tail"}, timeout=60).json()
while job["status"] not in ("completed", "failed", "cancelled"):
time.sleep(2)
job = requests.get("https://inference.mashbox.gg/v1/jobs/" + job["id"], headers=H, timeout=60).json()
print(job)Maya1 text to speech
Model For approved accounts
Text in, speech out, in a voice you describe in plain words. POST /v1/audio/speech with model maya1 answers with the audio itself. Built on Maya1 by Maya Research.
Request fields
| Field | Type | Required | About |
|---|---|---|---|
input | string | yes | Text to speak, 1 to 4,096 characters. Put any of the tags below anywhere in the text; the server never adds, removes or moves a tag. |
instructions | string | no | Tone, emotion or style, added to the end of the voice description. |
model | string | yes | maya1 |
response_format | string | yes | Send wav or pcm on every request: 24 kHz, mono, 16-bit. Other formats are not served for this model yet. |
stream_format | string | no | audio = the audio bytes, streamed as they are made; sse = server-sent events speech.audio.delta {audio: base64} then speech.audio.done {usage}. One of audio, sse. Default audio. |
voice | string or object | yes | male or female (neutral presets), {"id": "male"} or {"id": "female"}, or {"description": "..."}: a voice you describe in plain words, 1 to 1,000 characters. The same description always gives the same voice. |
curl https://inference.mashbox.gg/v1/audio/speech \
-H "Authorization: Bearer $MASHBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "maya1", "input": "Hold the gate! They'\''re coming over the ridge.", "voice": {"description": "A gruff old blacksmith with a deep, slow voice."}, "response_format": "wav"}' \
--output speech.wavOne request makes up to 120 seconds of audio. The male and female presets are neutral: emotion comes from the tags below and from emotion and intensity words in a voice description, for example an excited young woman, high intensity.
Emotion tags
Put a tag where the sound or change of delivery should happen. Results depend on the text, the voice description and where the tag sits, so try a few placements. Only these 20 tags work.
| Tag | Example input |
|---|---|
<angry> | You took the last boat without asking. <angry> That was ours. |
<appalled> | <appalled> You left the generator running all night with the door shut? |
<chuckle> | Well, that went about as well as I expected. <chuckle> Next time, read the map. |
<cry> | I waited at the dock until it got dark. <cry> Nobody came. |
<curious> | <curious> What's that light out past the breakwater? It wasn't there yesterday. |
<disappointed> | We drove four hours for this. <disappointed> The museum's closed on Mondays. |
<excited> | <excited> They found the radio! It still works, listen to this! |
<exhale> | Okay. <exhale> We're through the worst of it now. |
<gasp> | Turn around slowly. <gasp> Don't move, it's right behind you. |
<giggle> | You put salt in the coffee again. <giggle> I'm not drinking that. |
<gulp> | The bridge is out, so we go across the ice. <gulp> Right. Sure. |
<laugh> | He tried to fix the roof with duct tape. <laugh> In November. |
<laugh_harder> | And then the goat ate his hat. <laugh_harder> The whole hat! |
<mischievous> | <mischievous> I might have borrowed the key to the lighthouse. |
<sarcastic> | <sarcastic> Oh great, another storm warning. What a surprise. |
<scream> | Get away from the edge! <scream> Now! |
<sigh> | I fixed the fence again this morning. <sigh> The deer will be back by Friday. |
<sing> | <sing> Row, row, row your boat, gently down the stream. |
<snort> | <snort> You call that a fish? I've seen bigger bait. |
<whisper> | <whisper> Keep your voice down. They're sleeping in the next room. |
Real-time voice conversation, speech in and speech out: coming soon.
Jobs
Creatures run as jobs. The request answers at once with the job's id and status. Poll GET /v1/jobs/{id} about every 2 seconds. Statuses: queued, in_progress, moderating, completed, failed, cancelled. Cancel with POST /v1/jobs/{id}/cancel.
- Idempotency-Key: send the same key to retry a request safely.
- Outputs: signed URLs that last 10 minutes. Fresh ones come from
GET /v1/jobs/{id}/files. Outputs are kept 7 days.
Errors
Errors use the OpenAI error shape, with extra fields that say what to do next: request_id, limit_source, remedy, resets_at and docs, a link to the row below. Busy answers are 429 with Retry-After; wait that many seconds and retry. A model_loading answer also carries error.loading with state and eta_s. insufficient_quota is a 429 without Retry-After: retrying does not help.
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_json | The body is not valid JSON for this route, or the Content-Type is not application/json. | Send a JSON body with Content-Type: application/json. |
| 400 | invalid_request | The request is malformed for this route. | Check the route's request schema. |
| 400 | invalid_parameter | A parameter is missing, of the wrong type or out of range; param names it. | Fix the named parameter. |
| 400 | unsupported_parameter | This model does not take one of the parameters you sent; param names it. | Remove it. GET /v1/models lists each model's supported_parameters. |
| 400 | unknown_parameter | The request includes an argument the API does not recognize; the message names it. Nothing was run. | Remove the unrecognized argument. |
| 400 | invalid_value | A parameter has a value outside the set this model serves (OpenAI's invalid_value wording); param names it and the message lists the supported values. | Send one of the listed values. |
| 400 | unsupported_shape | The size, duration or shape is not one this model serves. | Use one of the values listed in the message or the model's input_schema. |
| 400 | precision_unavailable | The asked precision tier is not offered for this model. | Ask for a listed precision (GET /v1/models precisions) or none for the default. |
| 400 | prompt_too_long | The prompt is over the model's length limit. | Shorten the prompt. |
| 400 | invalid_input_file | The uploaded input file is missing, unfinished or not valid for this model. | Upload again (POST /v1/uploads, then PUT the exact bytes) and pass its id. |
| 401 | invalid_api_key | The API key or session is missing or not valid. | Send Authorization: Bearer <key>. |
| 401 | api_key_expired | The API key has expired. | Create a new key. |
| 401 | session_expired | The browser session has expired. | Sign in again. |
| 401 | recent_sign_in_required | Creating a key needs a sign-in within the last 15 minutes. | Sign in again, then retry. |
| 403 | account_paused | The account is paused. | Contact support with the request id. |
| 403 | human_check_failed | The human check (Turnstile) is missing or failed. | Complete the check and retry once. |
| 403 | key_disabled | This key is disabled. | Enable it or use another key. |
| 403 | model_not_allowed | This key is not allowed to use this model. | Add the model to the key's allowed_models. |
| 403 | not_permitted | This action is not permitted for this account or key. | See the message. |
| 403 | origin_not_allowed | API keys are not accepted from browsers (an Origin header was sent). | Call the API from your server; the web app uses sessions. |
| 404 | route_not_found | No such route. | Check the path against these docs. |
| 410 | link_expired | The email link has expired or was already used. | Sign up again to get a new link. |
| 429 | evaluation_quota_reached | The free evaluation quota for this model is used up for the rolling window (resets_at says when it frees up). | Talk to us for more. |
| 429 | demo_daily_capacity_reached | Today's free evaluation capacity for this model is used up. It comes back at 00:00 UTC. | Retry after resets_at, or talk to us for dedicated capacity. |
| 404 | model_not_found | No model with this id (or it does not serve this endpoint). | List models with GET /v1/models. |
| 404 | job_not_found | No job with this id for this account. | Check the id. |
| 404 | not_found | The object does not exist. | Check the id. |
| 405 | method_not_allowed | This route does not accept that HTTP method. | Check the method against these docs. |
| 409 | idempotency_key_in_flight | A request with this Idempotency-Key is still being admitted. | Retry after Retry-After; the first request's job is returned then. |
| 409 | job_not_cancellable | The job already finished. | Nothing to do. |
| 409 | not_ready | The video is not finished yet. | Poll until status is completed. |
| 413 | request_too_large | The request body is over this route's size limit. | Send a smaller body; upload files with POST /v1/uploads. |
| 413 | file_too_large | The file is over the size limit. | Send a smaller file. |
| 403 | bad_signature | The signed file URL is wrong or has expired. | Ask for a fresh URL (GET /v1/jobs/{id}/files or a new upload). |
| 403 | evaluation_access_required | This model is open to approved evaluation accounts only. | Request free evaluation access at https://mashbox.gg/access. |
| 409 | upload_not_pending | This upload URL was already used. | Create a new upload. |
| 422 | idempotency_key_reused | This Idempotency-Key was used with a different body (keys are kept 24 h). | Use a new key for a new request. |
| 410 | output_expired | The output is past its retention period. | Run the job again. |
| 415 | unsupported_media_type | This file type is not accepted here. | Use one of the types named in the message. |
| 429 | rate_limit_exceeded | Too many requests per minute for the key, account or network (limit_source says which). | Wait Retry-After seconds. |
| 429 | concurrency_limit_exceeded | Too many requests in flight at once. | Wait for one to finish. |
| 429 | demo_budget_exhausted | Today's demo budget is used up. | Sign in for the free tier. |
| 429 | capacity_busy | Every slot serving this model is busy. error.capacity gives slots_busy, queued_ahead and eta_s, the seconds until a slot is expected to be free; eta_basis says how that time was worked out. | Retry after Retry-After seconds. |
| 429 | capacity_unavailable | This tier is not available right now. | Use the tier named in the message, or retry after Retry-After. |
| 429 | model_unavailable | The model is paused or not served right now. | Retry after Retry-After seconds. |
| 429 | queue_full | Every slot serving this model is busy and its queue is full. error.capacity has the same fields as for capacity_busy. | Retry after Retry-After seconds. |
| 429 | model_loading | The model is loading. error.loading gives its state and eta_s, the seconds until it is ready; eta_basis says how that time was worked out. | Retry after Retry-After seconds. |
Limits
Each account and each key has a request rate and a number of runs in flight. Every response carries X-RateLimit-Limit-Requests, X-RateLimit-Remaining-Requests and X-RateLimit-Reset-Requests. GET /v1/limits returns your current limits as JSON. Over a limit you get 429 with Retry-After.
Evaluation quota
Each approved company gets 10 creatures and 30 minutes of audio per 30 days, free. Need more? Write to [email protected]. When it is used up, requests answer 429.
When a demo is paused
A paused demo answers 429 model_unavailable with Retry-After. GET /v1/status shows which demos are live.
OpenAI compatibility
Text to speech works with the official OpenAI SDKs: point the client at https://inference.mashbox.gg/v1 and pass your MASHBOX key. Errors use the OpenAI error shape. Creatures have no OpenAI equivalent; call them over HTTP.
from openai import OpenAI
import os
client = OpenAI(base_url="https://inference.mashbox.gg/v1", api_key=os.environ["MASHBOX_API_KEY"])
with client.audio.speech.with_streaming_response.create(
model="maya1",
input="Hold the gate! They're coming over the ridge.",
voice={"description": "A gruff old blacksmith with a deep, slow voice."},
response_format="wav",
) as r:
r.stream_to_file("speech.wav")Models
GET /v1/models lists what the API serves right now. It's public: no key needed.
curl https://inference.mashbox.gg/v1/modelsStatus
GET /v1/status gives each model's live state. Public, no key needed.
curl https://inference.mashbox.gg/v1/statusPacks
The other 11 catalog models are available as Packs, with example outputs for each on the Packs page. Write to [email protected].
Measured results are on the research page.