MiaoCut API
Remove image backgrounds from your own apps. The API runs on the same pipeline as the website's batch tool, so credits, result retention and refunds work the same way.
Base URL: https://pro-api.miaocut.app. All requests and responses are JSON, except the image upload and download links.
Quick start
- Create an API key on the Developer API page (sign-in required).
- Create a job with the file names, and get an upload link for each image.
- Upload each image to its link, then start the job.
- Poll the job until it is done, then download the transparent PNG results.
Authentication
Send your key in the Authorization header: Authorization: Bearer mc_live_YOUR_API_KEY. Keys start with mc_live_. The full key is shown only once when you create it. Keep it secret: anyone with the key can spend your credits. If a key leaks, revoke it on the Developer API page and it stops working immediately.
Credits
API calls use the same credits as the website. Credits are reserved for the whole job when you call start. Images that fail to process are refunded automatically.
| profile | Mode | Credits per image |
|---|---|---|
fast | Standard mode | 1 |
fur | Fine edges mode (hair, fur, plush, plants) | 2 |
Endpoints
1. Create a job
POST /api/v1/jobs with a JSON body. Nothing is charged yet.
{
"profile": "fast",
"files": [{ "name": "photo-1.jpg" }, { "name": "photo-2.png" }]
}
Response:
{
"job_id": "3f0c9a52-…",
"profile": "fast",
"cost_per_image": 1,
"total_cost": 2,
"expires_in": 3600,
"uploads": [
{ "image_id": "…", "orig_filename": "photo-1.jpg", "put_url": "https://…" },
{ "image_id": "…", "orig_filename": "photo-2.png", "put_url": "https://…" }
]
}
The number of images per job depends on your plan (up to 200 on the free tier and Basic, up to 500 on Pro and Business). If your balance is lower than total_cost you get 402 insufficient_credits.
2. Upload the images
PUT the raw image bytes to each put_url, in the same order as files. No Authorization header is needed (and do not add one): the link carries its own signature and is valid for expires_in seconds (1 hour). Supported formats: PNG, JPEG and WebP. Images larger than 60 megapixels fail.
3. Start the job
POST /api/v1/jobs/{{job_id}}/start reserves the credits and queues the images. Response: {{ "status": "processing", "total": 2, "enqueued": 2 }}. A job can be started only once (409 already_started).
4. Check progress and download
GET /api/v1/jobs/{{job_id}}. Poll every 2–5 seconds. The first images can take up to a minute while a GPU starts. When done is true, every image has finished.
{
"job_id": "3f0c9a52-…",
"profile": "fast",
"status": "processing",
"total": 2,
"succeeded": 1,
"failed": 0,
"done": false,
"images": [
{ "image_id": "…", "idx": 0, "filename": "photo-1.jpg", "status": "done", "download_url": "https://…" },
{ "image_id": "…", "idx": 1, "filename": "photo-2.png", "status": "queued" }
],
"retention_days": 1,
"retention_ends_at": "2026-10-21T08:00:00.000Z"
}
Image status is queued, processing, done or failed. A done image has a download_url (a transparent PNG, valid for 1 hour; call this endpoint again for a fresh link). A failed image has an error and its credits are refunded. Results are kept for 1 day (7 days with a subscription), then deleted.
Limits
- 60 requests per minute per key. Over the limit you get
429 rate_limitedand aRetry-Afterheader (seconds). - Up to 5 jobs in progress per key (processing, or created within the last hour and not started yet). Over the limit you get
429 too_many_active_jobs. - Up to 5 active keys per account.
- A key can only see and start jobs created through the API.
Errors
Errors are JSON like {{ "error": "insufficient_credits", "balance": 3, "needed": 10 }}.
| Status | error | Meaning |
|---|---|---|
| 400 | no_files, too_many_images | No files in the request, or more images than your plan allows (max is returned). |
| 401 | invalid_api_key | The key is missing, malformed, unknown or revoked. |
| 402 | insufficient_credits | Not enough credits (balance and needed are returned when creating a job). |
| 404 | not_found | The job does not exist, or was not created through the API. |
| 409 | already_started | The job was already started. |
| 429 | rate_limited, too_many_active_jobs | Slow down and retry after the Retry-After seconds. |
| 503 | api_disabled, api_unavailable, enqueue_failed | Temporarily unavailable, retry later. enqueue_failed refunds the reserved credits. |
Examples
curl
KEY=mc_live_YOUR_API_KEY
API=https://pro-api.miaocut.app
# 1) create the job
curl -s "$API/api/v1/jobs" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"profile":"fast","files":[{"name":"photo.jpg"}]}'
# 2) upload the image to the put_url from the response (no Authorization header)
curl -s -X PUT --upload-file photo.jpg "PUT_URL_FROM_RESPONSE"
# 3) start, then 4) poll
curl -s -X POST "$API/api/v1/jobs/JOB_ID/start" -H "Authorization: Bearer $KEY"
curl -s "$API/api/v1/jobs/JOB_ID" -H "Authorization: Bearer $KEY"
Python
import time, requests
API = "https://pro-api.miaocut.app"
HEADERS = {"Authorization": "Bearer mc_live_YOUR_API_KEY"}
def remove_backgrounds(paths, profile="fast"):
job = requests.post(f"{API}/api/v1/jobs", headers=HEADERS, json={
"profile": profile, "files": [{"name": p} for p in paths]}).json()
for path, up in zip(paths, job["uploads"]):
with open(path, "rb") as f:
requests.put(up["put_url"], data=f).raise_for_status() # no Authorization header here
requests.post(f"{API}/api/v1/jobs/{job['job_id']}/start", headers=HEADERS).raise_for_status()
while True:
st = requests.get(f"{API}/api/v1/jobs/{job['job_id']}", headers=HEADERS).json()
if st["done"]:
return st["images"] # each "done" image has a download_url
time.sleep(3)
Node.js (18+)
import { readFile } from "node:fs/promises";
const API = "https://pro-api.miaocut.app";
const headers = { Authorization: "Bearer mc_live_YOUR_API_KEY" };
const call = (path, init = {}) => fetch(API + path, { ...init, headers: { ...headers, ...init.headers } }).then((r) => r.json());
const job = await call("/api/v1/jobs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ profile: "fast", files: [{ name: "photo.jpg" }] }),
});
await fetch(job.uploads[0].put_url, { method: "PUT", body: await readFile("photo.jpg") }); // no Authorization header
await call(`/api/v1/jobs/${job.job_id}/start`, { method: "POST" });
let status;
do {
await new Promise((r) => setTimeout(r, 3000));
status = await call(`/api/v1/jobs/${job.job_id}`);
} while (!status.done);
console.log(status.images);