# MiniAPI docs Base URL: https://api.minicut.ai. Every model's body is in https://api.minicut.ai/v1/openapi.json. # Quickstart Three steps from no account to your first video. ## Get an API key Sign in at api.minicut.ai and open Keys. Press Create key. The key is shown once, so copy it into your environment. [Create API key](/console/keys) ```bash .env MINIAPI_KEY=mapi_live_... ``` ## Add funds Every account is prepaid. Add $10, $25, $50, $100, or any whole amount up to $10,000. A request needs your balance to cover its price. You are not charged for a job that fails. [Add $10](/console/billing/add?amount=10) [See prices](/pricing) ## Send your first request Submit a job to a model id, then read its status until it finishes. This one is 5 seconds of Seedance 2.5 at 720p, which costs $1.75. ```bash cURL # 1. Submit curl -X POST "https://api.minicut.ai/v1/queue/seedance-2.5" \ -H "Authorization: Key $MINIAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt":"a red fox runs through snow at dawn","resolution":"720p","duration":"5"}' # 2. Status, until it says COMPLETED curl "https://api.minicut.ai/v1/queue/seedance-2.5/requests/$REQUEST_ID/status" \ -H "Authorization: Key $MINIAPI_KEY" # 3. Result curl "https://api.minicut.ai/v1/queue/seedance-2.5/requests/$REQUEST_ID" \ -H "Authorization: Key $MINIAPI_KEY" ``` ```python Python import os, time, requests BASE = "https://api.minicut.ai/v1/queue/seedance-2.5" H = {"Authorization": f"Key {os.environ['MINIAPI_KEY']}"} job = requests.post(BASE, headers=H, json={ "prompt": "a red fox runs through snow at dawn", "resolution": "720p", "duration": "5", }).json() while requests.get(job["status_url"], headers=H).json()["status"] != "COMPLETED": time.sleep(3) print(requests.get(job["response_url"], headers=H).json()) ``` ```js JavaScript const BASE = "https://api.minicut.ai/v1/queue/seedance-2.5"; const H = { Authorization: `Key ${process.env.MINIAPI_KEY}`, "Content-Type": "application/json" }; const job = await (await fetch(BASE, { method: "POST", headers: H, body: JSON.stringify({ prompt: "a red fox runs through snow at dawn", resolution: "720p", duration: "5", }) })).json(); let s; do { await new Promise((r) => setTimeout(r, 3000)); s = await (await fetch(job.status_url, { headers: H })).json(); } while (s.status !== "COMPLETED"); console.log(await (await fetch(job.response_url, { headers: H })).json()); ``` The submit answers at once with a request id and three addresses: ```json 200 OK { "request_id": "0192f3a4-5b6c-7d8e-9f01-23456789abcd", "status": "IN_QUEUE", "queue_position": 0, "status_url": "https://api.minicut.ai/v1/queue/seedance-2.5/requests/0192f3a4-.../status", "response_url": "https://api.minicut.ai/v1/queue/seedance-2.5/requests/0192f3a4-...", "cancel_url": "https://api.minicut.ai/v1/queue/seedance-2.5/requests/0192f3a4-.../cancel", "price": "1.750000", "currency": "USD" } ``` > **Skip the polling.** Add `?webhook_url=https://...` to the submit and we POST the result to you when the job ends, signed with your account's webhook secret. See [Webhooks](/docs/webhooks). --- # Authentication Every call to /v1 carries your API key in the Authorization header. ## Send your key Put the key after the word `Key`. `Bearer` works too, so clients written for other APIs need no change. ```bash cURL curl "https://api.minicut.ai/v1/balance" \ -H "Authorization: Key $MINIAPI_KEY" ``` ## Keys Create keys in the console under Keys. A key is shown once, when you create it; after that the console shows only its first characters, so copy it into your environment straight away. Each key can have its own daily or monthly spend limit. [Manage keys](/console/keys) ## Keep it secret Call the API from your server, never from a browser or an app a user can open. If a key leaks, revoke it in the console. It stops working from the next request. ## When it fails | HTTP | `error.code` | Why | |---|---|---| | 401 | `missing_api_key` | No `Authorization` header | | 401 | `invalid_api_key` | The key is wrong, unknown or revoked | | 403 | `account_frozen` | The account is frozen | All error codes are on [Errors](/docs/errors). --- # Balance and top-ups MiniAPI is prepaid. You add money, and each job's price comes out of your balance. ## Add money Add funds in the console with a card: $10, $25, $50, $100, or any whole amount up to $10,000. Money you add also moves you up the [rate limit steps](/docs/rate-limits). [Add $10](/console/billing/add?amount=10) [See prices](/pricing) ## What a job costs A job's price is fixed when you submit it, from the [price list](/pricing). To see it first, send the same body to the quote endpoint. A quote is free. ```bash cURL curl -X POST "https://api.minicut.ai/v1/quote/seedance-2.5" \ -H "Authorization: Key $MINIAPI_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "a paper boat on a lake", "resolution": "720p", "duration": 5}' ``` When you submit, the price is set aside from your balance while the job runs. If the job completes, that is the charge. If it fails, is cancelled or expires, the money comes back. You are not charged for it. ## Read your balance ```bash cURL curl "https://api.minicut.ai/v1/balance" \ -H "Authorization: Key $MINIAPI_KEY" ``` | Field | Meaning | |---|---| | `balance` | Your balance, with running jobs already taken out | | `held` | In use by running jobs | | `available` | What you can spend now | | `lifetime_added` | Everything you have added | | `month_spend` | Spent this month, against `monthly_cap` | | `currency` | `USD`. Every amount on this reply is in US dollars | A submit whose price is more than `available` answers 402 `insufficient_balance` and starts nothing. --- # Model ids Each model has one id. It goes in the path when you submit or ask for a quote. ## Find an id The [price list](/pricing) names every model. `GET /v1/models` lists them with their ids, settings and prices; `GET /v1/models/{model_id}` gives one. ```bash cURL curl "https://api.minicut.ai/v1/models" \ -H "Authorization: Key $MINIAPI_KEY" ``` ## Use an id The id goes in the path. Status, result and cancel take the same id and the `request_id` the submit gave you. | Call | Path | |---|---| | Submit | `POST /v1/queue/{model_id}` | | Quote | `POST /v1/quote/{model_id}` | | Status | `GET /v1/queue/{model_id}/requests/{request_id}/status` | | Result | `GET /v1/queue/{model_id}/requests/{request_id}` | | Cancel | `PUT /v1/queue/{model_id}/requests/{request_id}/cancel` | Each model takes its own fields. The [API reference](/docs/api) shows them: pick a model there and the body and the code change with it. A field the model does not take is refused with 422. ## On sale and coming soon `status` in `/v1/models` is `on_sale` or `coming_soon`. A coming soon model has a price but cannot take jobs yet; a submit answers 503 `model_unavailable` and you are not charged. --- # Webhooks Get the result pushed to you when a job ends, instead of polling its status. ## Two ways to get them - **Per request.** Add `?webhook_url=https://your.app/hooks` to the submit. That request's events go there. - **Per account.** Add an endpoint in the console under Webhooks. Every request's events go there. You can pick which events it gets. A request with a `webhook_url` on an account with endpoints sends to all of them, with the same `webhook-id`. ## Events | Event | When | |---|---| | `request.completed` | The job finished. `data.payload` has the result, as the result endpoint gives it. | | `request.failed` | The job failed. You are not charged for it. `data.error.code` says why. | | `request.cancelled` | You cancelled it. You are not charged for it. | | `request.expired` | It did not finish within 24 hours. You are not charged for it. | ```json request.completed { "type": "request.completed", "timestamp": "2026-10-12T18:02:11.482Z", "data": { "request_id": "0192f3a4-5b6c-7d8e-9f01-23456789abcd", "model_id": "seedance-2.5", "status": "COMPLETED", "state": "completed", "payload": { "video": { "url": "https://...", "content_type": "video/mp4", "file_size": 4183920, "width": 1280, "height": 720, "fps": 24, "duration": 5.04 }, "seed": null }, "error": null, "billing": { "price": "1.750000", "currency": "USD" }, "created_at": "2026-10-12T18:01:02.001Z", "completed_at": "2026-10-12T18:02:11.002Z" } } ``` File links in a webhook work for 24 hours from that delivery. After that, read the result endpoint for fresh ones. ## Verify the signature Every delivery follows [Standard Webhooks](https://www.standardwebhooks.com). Three headers come with it: | Header | What | |---|---| | `webhook-id` | `msg_` and an id, the same on every retry of one event. Use it to drop repeats. | | `webhook-timestamp` | Unix seconds when this attempt was sent. | | `webhook-signature` | `v1,` and a base64 HMAC-SHA256 of `id.timestamp.body`. Two of them, space-separated, for 24 hours after you rotate your secret. | Get your secret from `GET /v1/webhooks/default/secret` or from the console, then check every delivery with the Standard Webhooks library before you trust it. ```js Node import { Webhook } from "standardwebhooks"; const wh = new Webhook(process.env.MINIAPI_WEBHOOK_SECRET); // whsec_... app.post("/hooks", express.raw({ type: "application/json" }), (req, res) => { const event = wh.verify(req.body.toString(), req.headers); // throws if it does not match res.sendStatus(200); // then handle event.type and event.data }); ``` ```python Python from standardwebhooks.webhooks import Webhook wh = Webhook(os.environ["MINIAPI_WEBHOOK_SECRET"]) # whsec_... @app.post("/hooks") def hooks(request): event = wh.verify(request.body, request.headers) # raises if it does not match return "", 200 ``` Use the raw body exactly as it arrived. Parsing and re-encoding the JSON first changes the bytes and the check fails. ## Answer fast, and retries Answer with any 2xx within 15 seconds, then do the work. Anything else counts as a failure: a 3xx is not followed, and a slower answer is a timeout. A failed delivery is tried again on this schedule, up to 10 attempts in about 3 days: | Attempts 1 to 5 | Attempts 6 to 10 | |---|---| | 1: now | 6: 5 hours later | | 2: 5 seconds later | 7: 10 hours later | | 3: 5 minutes later | 8: 10 hours later | | 4: 30 minutes later | 9: 24 hours later | | 5: 2 hours later | 10: 20 hours later | Every attempt shows in the console under Webhooks with its status code and how long it took. Press Resend on any of them to send that event again now, with the same `webhook-id`. An endpoint that fails every delivery for 5 days is turned off, and we email the account owner. Turn it back on in the console once it is fixed. ## Rotate the secret Press Rotate in the console. The new secret signs every delivery from then on, and the old one signs too for 24 hours, so you can update your receivers without missing an event. ## Addresses we refuse The URL must be https, with no user name or password in it. We resolve the name and refuse it if any answer is a private or internal address: loopback, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, link-local (including 169.254.169.254), 100.64.0.0/10, 0.0.0.0/8, and their IPv6 forms. A submit with such a `webhook_url` answers 400 `invalid_webhook_url`. --- # Idempotency Send an Idempotency-Key with a submit, and a retry never starts a second job or charges twice. ## Send a key Make a new key for each job, a UUID for example, and send it in the `Idempotency-Key` header. If the network drops before you get the answer, send the same request with the same key again. ```bash cURL curl -X POST "https://api.minicut.ai/v1/queue/seedance-2.5" \ -H "Authorization: Key $MINIAPI_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6c0d6a0e-3f0b-4f39-9d8f-1b2b8e0e7a11" \ -d '{"prompt": "a paper boat on a lake", "resolution": "720p", "duration": 5}' ``` ## What happens on a repeat | You send | You get | |---|---| | The same key and the same body, within 24 hours | The first answer again, the same `request_id`, with `Idempotent-Replayed: true`. Nothing new starts and nothing more is charged. | | The same key with a different body | 409 `idempotency_conflict` | | A key first used more than 24 hours ago | 409 `idempotency_conflict`. Use a new key. | | Two submits with one key at the same moment | One job. Both get its `request_id`. | Keys belong to your account, not to one API key, and are 1 to 255 characters. Without the header every submit is a new job. --- # Errors Every error has the same body. Read error.code to decide what to do. ## Error body ```json 402 Payment Required { "detail": "Your balance is $0.40 and this request costs $1.75. Add funds at https://api.minicut.ai/console/billing.", "error": { "type": "billing_error", "code": "insufficient_balance", "message": "Your balance is $0.40 and this request costs $1.75. Add funds at https://api.minicut.ai/console/billing.", "param": null, "request_id": "req_01JA9Y7Q2K4M5N6P7R8S9T0V1W" } } ``` A 422 `validation_error` lists every bad field in `detail`, and `error.param` names the first one: ```json 422 Unprocessable Content { "detail": [{ "loc": ["body", "duration"], "msg": "must be one of 5, 10", "type": "enum" }] } ``` Every response carries an `X-Request-Id`. Send it to us with any question about a call. ## Codes | HTTP | `error.code` | When | What to do | |---|---|---|---| | 400 | `invalid_json` | The body is not JSON | Fix the body | | 400 | `request_in_progress` | You asked for a result before the job ended | Poll the status, or use a webhook | | 400 | `already_completed` | You cancelled a job that already ended | Nothing to do | | 400 | `invalid_webhook_url` | `webhook_url` is not https, or points at a private address | Use a public https address | | 401 | `missing_api_key` | No `Authorization` header | Send `Authorization: Key ` | | 401 | `invalid_api_key` | The key is wrong, unknown or revoked | Make a new key in the console | | 402 | `insufficient_balance` | Your balance is under the price | Add funds | | 403 | `account_frozen` | The account is frozen | Email team@arkzero.ai | | 404 | `model_not_found` | No model has that id | See `GET /v1/models` | | 404 | `request_not_found` | No request of yours has that id | Check the id and the model id | | 405 | `method_not_allowed` | Wrong HTTP method for this path | See the `Allow` header | | 409 | `idempotency_conflict` | Same `Idempotency-Key`, different body | Use a new key for a new body | | 410 | `quote_expired` | The `X-Quote-Id` is older than 15 minutes or for another body | Get a new quote | | 413 | `payload_too_large` | Body over 1 MB, or an input file over 50 MB | Send smaller files, by URL | | 422 | `validation_error` | A field fails the model's settings | Fix the fields in `detail` | | 422 | `input_fetch_failed` | An input URL could not be fetched or is the wrong type | Check the URL is public and the file type | | 422 | `content_refused` | The content check refused the prompt or an input | Change the prompt or the input | | 429 | `rate_limited` | Too many requests this minute | Wait for `Retry-After` | | 429 | `key_spend_limit_reached` | This key hit the spend limit you set | Wait for `reset_at`, or raise the limit | | 429 | `monthly_cap_reached` | Your step's monthly cap is spent | Wait for `reset_at`, or add funds to move up a step | | 500 | `internal_error` | Something went wrong on our side | Try again; send us the request id | | 503 | `model_unavailable` | The model cannot take jobs right now | Try again after `Retry-After` (60 s). Nothing was charged | ## When a job fails A job that was accepted and then failed answers its status with `state` and `error.code`, its result with 422, and its webhook with `data.error.code`. You are not charged for any of these. | `error.code` | Meaning | |---|---| | `content_refused` | The result was refused by the content check | | `supplier_failed` | The model failed the job | | `supplier_unreachable` | The model could not be reached for too long | | `output_unavailable` | The result could not be saved | | `safety_check_unavailable` | The content check on the result could not run | | `internal_error` | The job stopped on our side before it finished | | `expired` | It did not finish within 24 hours | | `cancelled` | You cancelled it | --- # Rate limits Your limits go up with the total money you have ever added to your account. Your current balance does not count. You can see your step, and how much more takes you to the next one, in the console under Limits. ## Steps Adding $10 five times puts you on the same step as adding $50 once. | Step | Total added | Jobs at once | Submits a minute, per key | Monthly spend cap | |---|---|---|---|---| | 0 | $0 | 0 | 10 | $0 | | 1 | $10 | 2 | 10 | $100 | | 2 | $50 | 4 | 30 | $500 | | 3 | $100 | 6 | 60 | $2,000 | | 4 | $500 | 10 | 120 | $10,000 | | 5 | $2,000 | 16 | 300 | $50,000 | ## Reads Status, result, models, requests and balance: 600 a minute per key, at every step. ## How each limit works - **Jobs at once.** Jobs over the limit are not refused. They wait in line and start in the order you sent them. `queue_position` says how many of yours are ahead. - **Submits a minute.** Counted per key. Over the limit you get 429 with a `Retry-After` header. - **Monthly spend cap.** Resets on the 1st of each month, UTC. - **Key spend limit.** Optional. Set a spend limit per day or per month on any key in the console. ## Headers Every response carries `RateLimit-Policy` and `RateLimit`, so your code can slow down before it hits a 429. ```http 429 Too Many Requests HTTP/1.1 429 Too Many Requests Retry-After: 12 RateLimit-Policy: 10;w=60 RateLimit: limit=10, remaining=0, reset=12 { "detail": "Too many submits. Try again in 12 seconds.", "error": { "type": "rate_limit_error", "code": "rate_limited", "param": null, "request_id": "req_01JA9Y7Q2K..." } } ``` ## Errors | HTTP | `error.code` | When | |---|---|---| | 429 | `rate_limited` | Too many submits this minute. Wait for `Retry-After`. | | 429 | `key_spend_limit_reached` | This key hit the limit you set. The body has `reset_at`. | | 429 | `monthly_cap_reached` | Your step's monthly cap is spent. The body has `reset_at`. | | 402 | `insufficient_balance` | Step 0: add $10 to make your first request. | ## Higher limits Need more jobs at once or a higher cap? Send us a request with your business and monthly volume. Request higher limits at https://api.minicut.ai/docs/rate-limits.