Genjutsu API
Generate images and video from your own code with the same models, prices and wallet as the studio. All requests go to https://genjutsu.top; results are served from Genjutsu URLs.
New integrations should use /v1/media/*: see the Async media API and the quick start. /v1/generations and /v1/quotes are kept for compatibility.
/docs is the walkthrough and this page the reference. /openapi.yaml imports into Postman, Insomnia or openapi-generator; /skill.md is the same guide for AI coding tools.
Authentication
Send your key as Authorization: Bearer gj_live_…. Keys are shown once, stored only as a hash, and can be limited by scope (generate, read, assets, redeem), model, credit budget and expiry. Rotate or revoke them any time in the API console; revoked keys stop working immediately. Keys never grant administrative access. Keep keys on your server: never ship them in a web page or app bundle.
Async media API
The simplest way to generate. POST /v1/media/generate takes model, an optional prompt, params (the values listed under each model's params), an optional notify_url and an optional strategy (best, price, speed or success). It answers 202 with a task_id. Poll GET /v1/media/tasks/{task_id} every 5 seconds until is_final is true, or give a notify_url and we POST the same task body to it when the task is final (attempts at once, then after 10, 30 and 60 seconds). Callbacks are signed like webhooks with the secret from GET /v1/media/notify-secret.
# 1. Pick a model and read its parameters
curl https://genjutsu.top/v1/media/models?type=image -H "Authorization: Bearer $GENJUTSU_API_KEY"
# 2. Start a task (202 -> {"task_id": "...", "state": "queued", "credits_held": "0.250000"})
curl -X POST https://genjutsu.top/v1/media/generate \
-H "Authorization: Bearer $GENJUTSU_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{"model":"gpt-image-2-5","prompt":"a lighthouse at dusk","params":{"aspect_ratio":"16:9"}}'
# 3. Poll every 5 seconds until "is_final" is true
curl https://genjutsu.top/v1/media/tasks/$TASK_ID -H "Authorization: Bearer $GENJUTSU_API_KEY"Endpoints
Each key carries scopes. A request outside the key's scopes is rejected with 403 and is never charged.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /v1/media/models | any | Models with params and price ranges (full list, no cursor) |
| POST | /v1/media/generate | generate | Submit a task; 202 with task_id. Supports Idempotency-Key and notify_url |
| GET | /v1/media/tasks/{id} | read | Task state, outputs, credits |
| POST | /v1/media/tasks/{id}/cancel | generate | Cancel; cancellation is cancelled, requested or not_cancellable |
| GET |
Idempotency and retries
Send Idempotency-Key (1-200 characters of A-Za-z0-9._:-) on POST /v1/media/generate and POST /v1/generations. Within 24 hours, repeating the request with the same key and body returns the original generation (header Idempotent-Replayed: true) and never charges twice; the same key with a different body returns 409 IDEMPOTENCY_CONFLICT, and a retry while the first request is still being processed returns 409 IDEMPOTENCY_IN_PROGRESS with Retry-After. Keys are scoped to your account (not to one API key), so a retry sent after rotating the key still replays.
Limits and retention
- 60 requests per minute per key by default (adjustable per key) and 120 per minute per account. Successful responses and 429s carry
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(unix seconds); 429 also hasRetry-After. - Lists (
/v1/media/models,/v1/keys) return everything with no cursor;/v1/usageand/v1/assetsusecursor/next_cursor. - Concurrent generations per account are shared between the studio and the API.
- Redeem: 10 attempts per hour per account. Request bodies: 256 KiB.
- Outputs are streamed from the model provider and expire after 3 days: download them promptly. Result URLs expire after about 10 minutes, so request the task or
/resultagain for fresh links. - Webhook delivery history is kept 30 days.
Key management
A management key (create one in the API console) can list, create, update and delete the keys of its own account with /v1/keys, and is accepted nowhere else. A key can never be given more than the key creating it has: scopes, models, budget, rate limit, IP range and expiry are all capped. A normal key with "allow changes via API" can read and tune only itself.
There is no rotate call. To rotate without downtime, POST /v1/keys for a new key, deploy it, then DELETE /v1/keys/{id} the old one. Single-key rotation in the console stops the old key at once.
Endpoint reference
Errors
Errors always look like {"error":{"code":"…","message":"…","request_id":"req_…"}} and every response has an X-Request-Id header. Codes are stable; messages are for humans.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_JSONINVALID_REQUEST | Malformed request: body not a JSON object, missing or mistyped top-level field, bad query parameter or cursor, malformed Idempotency-Key. |
| 401 | UNAUTHENTICATED | Missing, wrong, revoked or expired key. |
| 402 | INSUFFICIENT_CREDITSKEY_BUDGET_EXCEEDEDSPEND_CAP_REACHED | Wallet, key budget or the account's daily spending limit too low: add credits with a redeem code, raise the key's budget, or raise the limit in account settings. |
| 403 | INSUFFICIENT_SCOPEMODEL_NOT_ALLOWEDBANNEDACCOUNT_SUSPENDEDGENERATION_SUSPENDED | Not permitted. |
| 404 | MODEL_NOT_FOUNDGENERATION_NOT_FOUND |
Webhooks
These are the account webhooks you add in the API console; the per-request notify_url of /v1/media/generate is separate (event media.task, secret from GET /v1/media/notify-secret, retries after 10, 30 and 60 seconds, same headers and signature). We POST generation.succeeded, generation.failed and generation.cancelled events with headers Genjutsu-Signature (v1=hex HMAC-SHA256 of timestamp + "." + raw body with your signing secret), Genjutsu-Timestamp, Genjutsu-Event-Id and Genjutsu-Event-Type. Reply 2xx within 10 seconds. Failed deliveries are retried after about 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours; redirects are not followed. Reject timestamps older than 5 minutes and skip event ids you already processed.
import crypto from 'node:crypto';
// Express example: use the RAW body, not re-serialized JSON.
app.post('/genjutsu/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.header('Genjutsu-Timestamp');
const sig = req.header('Genjutsu-Signature') ?? '';
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400); // replay window
const expected = crypto.createHmac('sha256', process.env.GENJUTSU_WEBHOOK_SECRET)
.update(`${ts}.${req.body.toString('utf8')}`).digest('hex');
const ok = sig.split(',').map((s) => s.trim()).some((s) =>
s.startsWith('v1=') && s.length === expected.length + 3 &&
crypto.timingSafeEqual(Buffer.from(s.slice(3)), Buffer.from(expected)));
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
if (alreadyProcessed(event.id)) return res.sendStatus(200); // de-duplicate retries
handle(event); // generation.succeeded | generation.failed | generation.cancelled
res.sendStatus(200);
});Quick start
export GENJUTSU_API_KEY="gj_live_xxxxxxxx_..."
# 1. Price it
curl -s https://genjutsu.top/v1/quotes \
-H "Authorization: Bearer $GENJUTSU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-image-2-5","inputs":{"prompt":"a lighthouse at dusk"}}'
# 2. Submit (safe to retry with the same Idempotency-Key)
curl -s -i https://genjutsu.top/v1/generations \
-H "Authorization: Bearer $GENJUTSU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234" \
-d '{"model":"gpt-image-2-5","inputs":{"prompt":"a lighthouse at dusk"},"quote_id":"<id from step 1>"}'
# 3. Poll, then fetch the result
curl -s https://genjutsu.top/v1/generations/<id> -H "Authorization: Bearer $GENJUTSU_API_KEY"
curl -s https://genjutsu.top/v1/generations/<id>/result -H "Authorization: Bearer $GENJUTSU_API_KEY"// POST /v1/quotes -> 200
{ "id": "<quote id>", "amount": "0.120000" }
// POST /v1/generations -> 202 (Retry-After header tells you when to poll)
{
"id": "<generation id>",
"status": "queued",
"links": { "self": "https://genjutsu.top/v1/generations/<id>", "result": "https://genjutsu.top/v1/generations/<id>/result" }
}
// GET /v1/generations/<id>/result -> 200 (links expire in about 10 minutes)
{ "outputs": [{ "type": "image", "url": "https://..." }] }
// Any error -> 4xx/5xx
{ "error": { "code": "...", "message": "...", "request_id": "req_..." } }Statuses and charges
queued: accepted and waiting for capacity. Credits are reserved.running: the model is working on it.finalizing: output is being stored and the charge settled.reconciling: we are confirming the outcome after an interrupted hand-off; it is never submitted twice.succeeded(charge captured),failedorcancelled(reservation released) are final.
Amounts are exact decimal strings in credits with six decimals; $1 = 10 credits. Example: a model priced at 0.03 credits per image costs "0.120000" for 4 images. When several outputs are requested, only delivered outputs are charged.
Versioning
Within /v1 we only add fields, endpoints, error codes and enum values, so ignore unknown fields. Every response carries a Genjutsu-Api-Version header (a date) with the behaviour revision. Breaking changes ship as a new version with at least 6 months of overlap. Renamed model ids keep working for at least 180 days.