Permulaan pantas API
Muke Labs mendedahkan REST API berasaskan tugas di https://api.mukelabs.com. Anda menyerahkan tugas penjanaan, menerima job id, dan poll (atau resolve) sehingga job mencapai status terminal. Semua badan permintaan dan respons adalah JSON.
Pengesahan
API key dikeluarkan di portal pembangun di https://portal.muke.name. Hantar key sebagai Bearer token pada setiap permintaan. Rahsia penuh ditunjukkan sekali semasa penciptaan — selepas itu portal hanya menyenaraikan prefiksnya.
Setiap key yang dicipta di portal membawa set scope tetap yang sama — scope tidak boleh dipilih mengikut key. Key boleh dicipta dengan cap masa tamat tempoh dan boleh dibatalkan pada bila-bila masa.
- Pengurusan webhook menggunakan scope khusus (webhook:endpoint:write, webhook:delivery:read, webhook:delivery:replay) yang diperuntukkan secara berasingan — key portal swalayan tidak membawanya. Urus endpoint webhook di portal; lihat Webhooks di bawah.
Authorization: Bearer <your-api-key>| Scope | Membenarkan | Endpoint |
|---|---|---|
| generation:write | Hantar, resolve dan batalkan tugas; urus aset output | POST /v1/generations · POST /v1/generations/resolve · POST /v1/generations/{id}/cancel · endpoint kitar hayat aset |
| generation:read | Poll status job, main semula strim teks, baca katalog model | GET /v1/generations/{id} · GET /v1/generations/{id}/text-stream · GET /v1/catalog · GET /v1/catalog/status |
| usage:read | Pelaporan penggunaan | GET /v1/usage |
Cipta tugas penjanaan
Hantar POST /v1/generations dengan badan JSON. API menjawab 202 Accepted dengan job id; penjanaan berjalan secara asynchronous.
Menambah ?wait=true meminta gateway menahan permintaan dan poll sebentar bagi pihak anda untuk alat yang menyokong tunggu segerak. Apabila job mencapai status terminal tepat pada masanya, respons ialah objek Job penuh dan bukannya penerimaan 202.
curl -X POST https://api.mukelabs.com/v1/generations \
-H "Authorization: Bearer $MUKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"toolId": "seedream-5-pro",
"inputMode": "text-to-image",
"prompt": "a lantern-lit night market, cinematic",
"params": { "aspect_ratio": "16:9" },
"references": [],
"idempotencyKey": "order-42-image-1",
"subjectId": "user_123"
}'{
"jobId": "job_7f3a…",
"run": { "id": "run_01…", "ordinal": 1 },
"acceptedAt": "2026-01-01T12:00:00.000Z",
"policy": { "profileCode": "default", "version": 1 },
"effectiveRetention": { "kind": "ttl", "ttlDays": 30 },
"deduplicated": false
}| Medan | Jenis | Wajib | Nota |
|---|---|---|---|
| toolId | string | ya | Pengenal model, cth. seedream-5-pro. Lihat halaman model atau GET /v1/catalog. |
| inputMode | string | ya | cth. text-to-image, image-to-video, chat, text-to-speech. Mestilah salah satu mod input yang dibenarkan model. |
| prompt | string | ya | Boleh jadi string kosong jika model tidak memerlukannya. |
| params | object | ya | Parameter model (nisbah aspek, tempoh, suara…). Mestilah objek JSON — hantar {} apabila tiada yang berkaitan. |
| references | array | ya | URL media rujukan. Hantar [] untuk mod teks sahaja. Setiap entri: {url, role?, ordinal?, kind?, expectedContentType?, expectedSha256?}; url mestilah HTTPS mutlak. |
| idempotencyKey | string | ya | ≤240 aksara, tanpa ruang kosong. Unik bagi setiap tugas logikal. |
| subjectId | string | ya | Pengenal pengguna akhir anda. Kuota dan polisi dikuatkuasakan mengikut subject. |
| variantId | string | tidak | Varian model apabila model mendedahkan beberapa. |
| messages | array | tidak | Konteks sembang, hanya untuk inputMode=chat. Peranan: system, user, assistant. |
| stream | boolean | tidak | Minta output teks berperingkat jika disokong (sembang sahaja). |
| retention | object|string|null | tidak | {ttlDays: n} atau "permanent" — penggantian penahanan output dalam had polisi. |
| webhookEndpointId | string | tidak | Sampaikan peristiwa terminal ke endpoint webhook berdaftar. |
| subjectPolicyVersion | integer | tidak | Tegaskan versi polisi yang terakhir anda perhatikan untuk subject. |
Poll keputusan
GET /v1/generations/{jobId} mengembalikan job. Poll sehingga status terminal: succeeded, failed atau cancelled. Titik permulaan yang dicadangkan ialah satu permintaan setiap 2–5 saat.
Job yang berjaya mendedahkan outputs — setiap satu dengan url bertandatangan, contentType, expiresAt, dan assetHandle untuk operasi kitar hayat.
- status: queued → running → succeeded | failed | cancelled
- POST /v1/generations/{jobId}/cancel membatalkan job dalam baris (mengembalikan JOB_ALREADY_STARTED apabila sudah berjalan).
- Url output bertandatangan dan luput — muat turun hasil ke storan anda sendiri sebelum expiresAt.
{
"id": "job_7f3a…",
"status": "succeeded",
"outputs": [
{
"assetHandle": "asset_…",
"url": "https://…/signed-download-url",
"contentType": "image/png",
"expiresAt": "2026-01-08T12:00:00.000Z"
}
],
"usage": { "billableUnits": 1, "processingMs": 8200 },
"createdAt": "2026-01-01T12:00:00.000Z",
"completedAt": "2026-01-01T12:00:08.000Z"
}Katalog model
GET /v1/catalog mengembalikan model yang diaktifkan untuk key anda berserta mod input yang dibenarkan. GET /v1/catalog/status melaporkan ketersediaan mengikut model dan mengikut mod input.
{
"scope": "tenant_base",
"revision": "rev_…",
"tools": [
{
"toolId": "seedream-5-pro",
"category": "image",
"allowedInputModes": ["text-to-image", "image-to-image"]
}
]
}Pelaporan penggunaan
GET /v1/usage meringkaskan job dan unit boleh bil akaun anda dalam julat masa (maksimum 366 hari). Memerlukan scope usage:read.
- groupBy: tool atau subject
- from / to: cap masa ISO 8601 dengan zon masa; julat mestilah positif dan ≤ 366 hari
curl "https://api.mukelabs.com/v1/usage?from=2026-01-01T00:00:00Z&to=2026-02-01T00:00:00Z&groupBy=tool" \
-H "Authorization: Bearer $MUKE_API_KEY"
# -> {
# "from": "…", "to": "…", "groupBy": "tool",
# "totals": { "jobs": 12, "billableUnits": 12 },
# "groups": [{ "key": "seedream-5-pro", "jobs": 12, "billableUnits": 12 }],
# "truncated": false
# }Idempotensi dan cubaan semula
Setiap penyerahan memerlukan idempotencyKey. Mengulang key yang sama dengan badan permintaan yang sama memainkan semula penerimaan 202 asal (deduplicated: true) — selamat untuk cuba semula selepas tamat masa atau kegagalan rangkaian. Menggunakan semula key dengan permintaan berbeza mengembalikan 409 IDEMPOTENCY_CONFLICT.
POST /v1/generations/resolve memulihkan penerimaan untuk permintaan terdahulu tanpa mencipta tugas pendua. Bentuk pilihan ialah menghantar semula badan permintaan penjanaan asal — gateway menghurai dan menormalkannya tepat seperti semasa penyerahan, kemudian membandingkan hash permintaan yang disimpan. Endpoint menjawab {status: "accepted", acceptance: {…}} atau {status: "fenced"} apabila tiada penerimaan sepadan. Jawapan fenced adalah kekal untuk key itu: ia merekodkan pagar negatif supaya POST asal yang tiba lewat tidak lagi boleh mencipta kerja, dan sebarang percubaan seterusnya di bawah key yang sama mengembalikan 409 IDEMPOTENCY_CONFLICT. Hantar semula di bawah idempotencyKey baharu.
Sebagai alternatif hantar tepat {idempotencyKey, requestHash} — tiada medan lain — di mana requestHash ialah heks SHA-256 bagi JSON kanonik permintaan. Kanonisasi: kunci objek disusun mengikut leksikograf pada setiap aras, sifat dengan nilai undefined digugurkan, susunan array dikekalkan, nombor mestilah terhingga, kedalaman sarang maksimum 32. Kira hash ke atas objek permintaan yang telah dihurai, bukan teks JSON mentah.
Hash dibandingkan dengan permintaan seperti yang dinormalkan oleh gateway, yang berbeza daripada badan mentah dalam dua kes: references[].expectedContentType dikecilkan huruf (hantar dalam huruf kecil), dan untuk keluarga model seedance-2 (seedance-2-0, seedance-2-standard, seedance-2-fast) gateway menyuntik parameter milik pelayan yang diperoleh daripada inputMode sebelum hashing — untuk model tersebut sentiasa resolve dengan badan permintaan asal, bukan hash yang dikira sendiri. Hash yang tidak sepadan dengan permintaan tersimpan mengembalikan 409 IDEMPOTENCY_CONFLICT.
import { createHash } from "node:crypto";
function canonicalJson(value, depth = 1, ancestors = new Set()) {
if (value === null || typeof value === "boolean" || typeof value === "string") {
return JSON.stringify(value);
}
if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
if (value && typeof value === "object") {
if (depth > 32) throw new Error("Generation request exceeds the maximum JSON depth of 32.");
if (ancestors.has(value)) throw new Error("Generation request contains a cyclic value.");
ancestors.add(value);
try {
if (Array.isArray(value)) {
return "[" + value.map((child) => canonicalJson(child, depth + 1, ancestors)).join(",") + "]";
}
return "{" + Object.entries(value)
.filter(([, child]) => child !== undefined)
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
.map(([key, child]) => JSON.stringify(key) + ":" + canonicalJson(child, depth + 1, ancestors))
.join(",") + "}";
} finally {
ancestors.delete(value);
}
}
throw new Error("Generation request contains a non-JSON value.");
}
const requestHash = createHash("sha256")
.update(canonicalJson(originalRequest), "utf8")
.digest("hex");Ralat
Ralat mengembalikan { "error": { "code", "message", "retryAfterSeconds?" } } dengan status HTTP yang sesuai. Ralat had kadar dan kuota mungkin mengandungi retryAfterSeconds.
| HTTP | Code | Maksud |
|---|---|---|
| 400 | INVALID_JSON / INVALID_REQUEST / VALIDATION_ERROR | Badan cacat atau medan gagal pengesahan |
| 401 | INVALID_KEY | API key tiada, tidak sah atau tidak aktif |
| 403 | INSUFFICIENT_SCOPE / TOOL_NOT_ALLOWED_BY_POLICY | Key tiada scope, atau polisi subject melarang alat itu |
| 404 | JOB_NOT_FOUND / NOT_FOUND / WEBHOOK_ENDPOINT_NOT_FOUND | Job, laluan atau endpoint webhook tidak diketahui |
| 409 | IDEMPOTENCY_CONFLICT | Kunci idempotensi telah digunakan untuk permintaan lain |
| 409 | JOB_ALREADY_STARTED | Hanya penjanaan dalam baris boleh dibatalkan |
| 409 | WEBHOOK_ENDPOINT_INVALID_STATE / WEBHOOK_ENDPOINT_LIMIT_REACHED / SUBJECT_POLICY_VERSION_MISMATCH | Endpoint webhook tidak aktif atau tidak dilanggan, pada had endpoint, atau versi polisi subject yang ditegaskan telah berubah |
| 413 | PAYLOAD_TOO_LARGE | Badan permintaan melebihi had |
| 415 | UNSUPPORTED_MEDIA_TYPE | Content-Type mestilah application/json |
| 422 | CONTENT_REJECTED / PROMPT_TOO_LONG / MEDIA_DURATION_INVALID / REFERENCE_COUNT_INVALID / REFERENCE_MEDIA_INVALID / CAPABILITY_REQUIRED / REQUEST_COST_LIMIT_REACHED | Permintaan terbentuk baik tetapi ditolak oleh kekangan model atau polisi |
| 429 | SUBJECT_LIMIT_REACHED / TOO_MANY_ACTIVE_GENERATIONS / DAILY_LIMIT_REACHED / TENANT_LIMIT_REACHED | Had kuota atau keserentakan dicapai — undur dan cuba semula |
| 500 | INTERNAL_ERROR | Penjanaan tidak dapat diselesaikan; tugas gagal dikembalikan secara automatik |
| 503 | GATEWAY_EXECUTION_DISABLED / CATALOG_UNAVAILABLE | Perkhidmatan tidak tersedia buat sementara — cuba semula dengan backoff |
Webhooks (pilihan)
Daripada polling, daftarkan endpoint webhook dan hantar webhookEndpointId semasa penyerahan. Apabila job mencapai status terminal, gateway menghantar POST peristiwa JSON bertandatangan ke URL endpoint anda. Sehingga 10 endpoint pending-atau-aktif dibenarkan setiap akaun.
Endpoint didaftarkan dan diuruskan di portal pembangun. Pendaftaran mengembalikan signingSecret sekali sahaja — simpan serta-merta. Pengaktifan melakukan handshake: Muke Labs menghantar POST bertandatangan dengan badan {"event":"webhook.challenge","challenge":"<id>"} dan endpoint anda mesti menjawab 2xx dengan badan JSON yang menggemakan nilai challenge yang sama ({"challenge":"<id>"}).
Penyerahan hanya boleh menyasarkan endpoint yang aktif dan melanggan sekurang-kurangnya satu peristiwa terminal — jika tidak penyerahan ditolak. Permukaan pengurusan yang sama wujud pada REST API di bawah scope khusus yang tidak dibawa oleh key swalayan: GET/POST /v1/webhook-endpoints, POST /v1/webhook-endpoints/{id}/activate, POST /v1/webhook-endpoints/{id}/secret/rotate, PUT /v1/webhook-endpoints/{id}/subscriptions, DELETE /v1/webhook-endpoints/{id} (scope webhook:endpoint:write); GET /v1/webhook-deliveries dan GET /v1/webhook-deliveries/{id} (webhook:delivery:read); POST /v1/webhook-deliveries/{id}/replay (webhook:delivery:replay).
Penghantaran yang tidak menerima respons 2xx dicuba semula sehingga 4 kali lagi (5 percubaan keseluruhan) pada lebih kurang 1 minit, 5 minit, 30 minit dan 2 jam, dengan jitter ±20%; header Retry-After pada respons 429 atau 503 dihormati sehingga satu jam. Selepas percubaan terakhir penghantaran ditandakan exhausted dan boleh dimainkan semula dari portal. Balas 2xx dengan cepat dan proses secara asynchronous.
Sahkan setiap penghantaran sebelum mempercayainya: kira semula tandatangan ke atas bait badan yang diterima tepat (bukan JSON yang diserialkan semula), tolak cap masa lebih tua daripada kira-kira 5 minit, dan deduplikasi pada x-muke-event-id. Kekalkan rahsia tandatangan sebelumnya diterima semasa putaran — penghantaran membawa versi rahsia yang digunakan untuk menandatanganinya.
- Jenis peristiwa: generation.succeeded, generation.failed, generation.cancelled
- data.job ialah objek Job yang sama dikembalikan oleh GET /v1/generations/{id} pada status terminal, termasuk outputs atau error
{
"id": "evt_…",
"version": "2026-07-22",
"type": "generation.succeeded",
"occurredAt": "2026-01-01T12:00:08.000Z",
"tenantId": "tenant_…",
"data": {
"run": { "id": "run_01…", "ordinal": 1 },
"job": {
"id": "job_7f3a…",
"status": "succeeded",
"outputs": [
{
"assetHandle": "asset_…",
"url": "https://…/signed-download-url",
"contentType": "image/png",
"expiresAt": "2026-01-08T12:00:00.000Z"
}
],
"usage": { "billableUnits": 1, "processingMs": 8200 },
"createdAt": "2026-01-01T12:00:00.000Z",
"completedAt": "2026-01-01T12:00:08.000Z"
}
}
}import { createHmac, timingSafeEqual } from "node:crypto";
// signed = base64url( HMAC-SHA256(secret, "<version>.<timestamp>.<eventId>." || rawBody) )
function signWebhook({ rawBody, eventId, timestamp, version, secret }) {
return createHmac("sha256", secret)
.update(`${version}.${timestamp}.${eventId}.`, "utf8")
.update(rawBody) // exact request bytes — verify before parsing JSON
.digest("base64url");
}
function verifyWebhook({ rawBody, headers, secretsByVersion, now = new Date(), maxAgeSeconds = 300 }) {
const eventId = headers["x-muke-event-id"];
const timestamp = headers["x-muke-timestamp"];
const version = Number(headers["x-muke-signature-version"]);
const signature = headers["x-muke-signature"];
const eventTime = Date.parse(typeof timestamp === "string" ? timestamp : "");
if (!Number.isFinite(eventTime) || Math.abs(now.getTime() - eventTime) > maxAgeSeconds * 1000) {
return false; // outside the replay window
}
const secret = secretsByVersion[version]; // keep current + previous secret during rotation
if (!secret || typeof signature !== "string") return false;
const expected = Buffer.from(signWebhook({ rawBody, eventId, timestamp, version, secret }), "utf8");
const actual = Buffer.from(signature, "utf8");
return expected.length === actual.length && timingSafeEqual(expected, actual);
}| Header | Nilai |
|---|---|
| content-type | application/json |
| x-muke-event-id | Pengenal peristiwa unik — guna untuk deduplikasi |
| x-muke-timestamp | Masa penghantaran ISO-8601, cth. 2026-01-01T12:00:08.123Z |
| x-muke-signature-version | Versi rahsia tandatangan (integer, bermula pada 1; meningkat semasa putaran) |
| x-muke-signature | base64url HMAC-SHA256 ke atas "{version}.{timestamp}.{eventId}." + bait badan mentah |