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.
Header
Authorization: Bearer <your-api-key>
ScopeMembenarkanEndpoint
generation:writeHantar, resolve dan batalkan tugas; urus aset outputPOST /v1/generations · POST /v1/generations/resolve · POST /v1/generations/{id}/cancel · endpoint kitar hayat aset
generation:readPoll status job, main semula strim teks, baca katalog modelGET /v1/generations/{id} · GET /v1/generations/{id}/text-stream · GET /v1/catalog · GET /v1/catalog/status
usage:readPelaporan penggunaanGET /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.

Request
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"
  }'
Response 202
{
  "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
}
MedanJenisWajibNota
toolIdstringyaPengenal model, cth. seedream-5-pro. Lihat halaman model atau GET /v1/catalog.
inputModestringyacth. text-to-image, image-to-video, chat, text-to-speech. Mestilah salah satu mod input yang dibenarkan model.
promptstringyaBoleh jadi string kosong jika model tidak memerlukannya.
paramsobjectyaParameter model (nisbah aspek, tempoh, suara…). Mestilah objek JSON — hantar {} apabila tiada yang berkaitan.
referencesarrayyaURL media rujukan. Hantar [] untuk mod teks sahaja. Setiap entri: {url, role?, ordinal?, kind?, expectedContentType?, expectedSha256?}; url mestilah HTTPS mutlak.
idempotencyKeystringya≤240 aksara, tanpa ruang kosong. Unik bagi setiap tugas logikal.
subjectIdstringyaPengenal pengguna akhir anda. Kuota dan polisi dikuatkuasakan mengikut subject.
variantIdstringtidakVarian model apabila model mendedahkan beberapa.
messagesarraytidakKonteks sembang, hanya untuk inputMode=chat. Peranan: system, user, assistant.
streambooleantidakMinta output teks berperingkat jika disokong (sembang sahaja).
retentionobject|string|nulltidak{ttlDays: n} atau "permanent" — penggantian penahanan output dalam had polisi.
webhookEndpointIdstringtidakSampaikan peristiwa terminal ke endpoint webhook berdaftar.
subjectPolicyVersionintegertidakTegaskan 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.
Response 200
{
  "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.

Response 200
{
  "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
Request
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.

requestHash (Node.js)
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.

HTTPCodeMaksud
400INVALID_JSON / INVALID_REQUEST / VALIDATION_ERRORBadan cacat atau medan gagal pengesahan
401INVALID_KEYAPI key tiada, tidak sah atau tidak aktif
403INSUFFICIENT_SCOPE / TOOL_NOT_ALLOWED_BY_POLICYKey tiada scope, atau polisi subject melarang alat itu
404JOB_NOT_FOUND / NOT_FOUND / WEBHOOK_ENDPOINT_NOT_FOUNDJob, laluan atau endpoint webhook tidak diketahui
409IDEMPOTENCY_CONFLICTKunci idempotensi telah digunakan untuk permintaan lain
409JOB_ALREADY_STARTEDHanya penjanaan dalam baris boleh dibatalkan
409WEBHOOK_ENDPOINT_INVALID_STATE / WEBHOOK_ENDPOINT_LIMIT_REACHED / SUBJECT_POLICY_VERSION_MISMATCHEndpoint webhook tidak aktif atau tidak dilanggan, pada had endpoint, atau versi polisi subject yang ditegaskan telah berubah
413PAYLOAD_TOO_LARGEBadan permintaan melebihi had
415UNSUPPORTED_MEDIA_TYPEContent-Type mestilah application/json
422CONTENT_REJECTED / PROMPT_TOO_LONG / MEDIA_DURATION_INVALID / REFERENCE_COUNT_INVALID / REFERENCE_MEDIA_INVALID / CAPABILITY_REQUIRED / REQUEST_COST_LIMIT_REACHEDPermintaan terbentuk baik tetapi ditolak oleh kekangan model atau polisi
429SUBJECT_LIMIT_REACHED / TOO_MANY_ACTIVE_GENERATIONS / DAILY_LIMIT_REACHED / TENANT_LIMIT_REACHEDHad kuota atau keserentakan dicapai — undur dan cuba semula
500INTERNAL_ERRORPenjanaan tidak dapat diselesaikan; tugas gagal dikembalikan secara automatik
503GATEWAY_EXECUTION_DISABLED / CATALOG_UNAVAILABLEPerkhidmatan 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
Delivery payload
{
  "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"
    }
  }
}
Verify a signature (Node.js)
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);
}
HeaderNilai
content-typeapplication/json
x-muke-event-idPengenal peristiwa unik — guna untuk deduplikasi
x-muke-timestampMasa penghantaran ISO-8601, cth. 2026-01-01T12:00:08.123Z
x-muke-signature-versionVersi rahsia tandatangan (integer, bermula pada 1; meningkat semasa putaran)
x-muke-signaturebase64url HMAC-SHA256 ke atas "{version}.{timestamp}.{eventId}." + bait badan mentah