Quick start
Generate images and video from your own code. The API uses the same models, prices and credit balance as the studio.
1. Base URL and authentication
The base URL is https://genjutsu.top (the site address, without /v1); every path below starts with /v1/... and is appended to it. Create a key in your account under Developer, keep it on your server, and send it on every request. Give the key the generate and read scopes: generate submits and cancels, read polls. Export it as GENJUTSU_API_KEY.
Authorization: Bearer $GENJUTSU_API_KEY2. Models
Pass one of these ids as model. The list is live. Prices and the full parameter list for each model are on the models page and, with a key, in GET /v1/media/models. Without a key use GET /api/public/models.
3. Submit a task
POST /v1/media/generate with the model id, a prompt and optional params. Find model ids and their parameters on the models page (or list them without a key at GET /api/public/models; with a key, GET /v1/media/models also returns each model's params). Send an Idempotency-Key (1-200 characters of A-Za-z0-9._:-) so a retry never charges twice; a repeat returns the original task with Idempotent-Replayed: true. The credits for the job are held right away and the response carries a task_id.
curl -s https://genjutsu.top/v1/media/generate \
-H "Authorization: Bearer $GENJUTSU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"model": "gpt-image-2-5",
"prompt": "a lighthouse at dusk"
}'// 202 Accepted
{ "task_id": "3f6c1a52-8d0e-4b7a-9c21-5e4d7a1b9f10", "state": "queued", "credits_held": "0.250000" }4. Video and references
Video models take duration in whole seconds, resolution (for example 720p) and aspect_ratio (for example 16:9) inside params, and are billed per second, so a longer clip costs more. Models with references also accept reference_images, reference_videos and reference_audios as lists. Each item is a public https URL or {"asset_id": "<uuid>"} for a ready file in your library. Ranges and file counts differ per model: Seedance 2.0 takes 4 to 15 seconds and up to 12 files, Seedance 2.5 and Wan 3.0 take 4 to 30 seconds, MiniMax H3 takes 5 to 15 seconds. Always read the model's params.
curl -s https://genjutsu.top/v1/media/generate \
-H "Authorization: Bearer $GENJUTSU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"model": "seedance-2.0",
"prompt": "slow dolly-in on a lighthouse at dusk, waves below",
"params": {
"duration": 8,
"resolution": "720p",
"aspect_ratio": "16:9",
"reference_images": ["https://your-cdn.example/lighthouse.png"],
"reference_videos": [{"asset_id": "<uuid of a ready video asset>"}],
"reference_audios": []
}
}'
# Parameter names, ranges and file limits differ per model: read them from GET /v1/media/models?type=video.5. Upload your own files
Needs a key with the assets scope. Allocate an upload, send the bytes, then finalize. The allocation answer has an upload.auth field that says how to authorize the PUT: same_as_request (the usual case) means send the same Authorization: Bearer key you used to allocate; none means the URL is pre-signed and you must send no Authorization header. Use the method and absolute url given and send the listed headers unchanged. Once the asset is ready, pass its id as {"asset_id": "<uuid>"}.
# 1. Allocate (scope: assets)
curl -s https://genjutsu.top/v1/assets/uploads \
-H "Authorization: Bearer $GENJUTSU_API_KEY" -H "Content-Type: application/json" \
-d '{"filename":"ref.mp4","mime":"video/mp4","bytes":4821733}'
# -> { "asset": { "id": "<uuid>", ... },
# "upload": { "method": "PUT", "url": "https://...", "headers": { "Content-Type": "video/mp4" },
# "auth": "same_as_request" | "none", "expires_at": "..." } }
# 2. PUT the bytes exactly as told.
# auth = same_as_request -> also send -H "Authorization: Bearer $GENJUTSU_API_KEY"
# auth = none -> pre-signed URL: send NO Authorization header
curl -s -X PUT "<upload.url>" -H "Content-Type: video/mp4" \
-H "Authorization: Bearer $GENJUTSU_API_KEY" --data-binary @ref.mp4
# 3. Finalize; the asset becomes "ready" (or "failed")
curl -s -X POST https://genjutsu.top/v1/assets/<uuid>/complete -H "Authorization: Bearer $GENJUTSU_API_KEY"6. Poll until final
GET /v1/media/tasks/{id} returns the state and, when finished, the output URLs. Poll about every 5 seconds and stop when is_final is true (needs the read scope). Failed tasks are refunded automatically. Output URLs are signed and expire within minutes: read the task again for fresh links. The outputs themselves expire after 3 days (see below).
curl -s https://genjutsu.top/v1/media/tasks/<task_id> \
-H "Authorization: Bearer $GENJUTSU_API_KEY"
# Repeat about every 5 seconds until the response has "is_final": true.7. Download and retention
Outputs are streamed to you from the model provider and are not archived. They expire after 3 days: download every result promptly and store it yourself. After that the task still reads as succeeded, but its links no longer return the file.
8. Or receive a webhook
Optional: add notify_url (public https, port 443 or 8443) to the submit body and we POST the final task to it (event type media.task). Delivery is attempted at once, then after 10, 30 and 60 seconds until you answer 2xx. Polling stays available as a fallback. These callbacks are separate from the account webhooks you add in the console (generation.* events, a longer retry schedule, per-endpoint secrets).
curl -s https://genjutsu.top/v1/media/generate \
-H "Authorization: Bearer $GENJUTSU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-image-2-5", "prompt": "a lighthouse at dusk",
"notify_url": "https://your-server.example/hooks/genjutsu"}'Each delivery carries Genjutsu-Signature: v1=<hex>, Genjutsu-Timestamp, Genjutsu-Event-Id and Genjutsu-Event-Type. The signature is HMAC-SHA256 over "<timestamp>.<raw body>" with the secret from GET /v1/media/notify-secret (scope read). Reject timestamps more than 300 seconds off and de-duplicate on Genjutsu-Event-Id.
import hmac, hashlib, time
def verify(secret: str, raw_body: bytes, ts: str, header: str) -> bool:
if not ts or abs(time.time() - int(ts)) > 300:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(p.strip()[3:], expected) for p in header.split(",") if p.strip().startswith("v1="))
# secret: GET https://genjutsu.top/v1/media/notify-secret (needs the read scope). De-duplicate on the Genjutsu-Event-Id header.9. Errors
Content refusals come back as content_policy_violation (HTTP 422, not retryable): the prompt is blocked before anything is charged, or the model refuses the input or output and the task ends failed with the same code and a full refund. Change the prompt or reference files and submit again.
Errors look like {"error": {"code", "message", "retryable", "request_id"}}. Do not retry permanent errors; retry transient ones with backoff and honour Retry-After. Quote the request_id (also the X-Request-Id header) to support. Limits: 60 requests per minute per key and 120 per minute per account. Successful responses and 429s carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds); a 429 also has Retry-After. Other errors (401, 402, 403, 422) carry only X-Request-Id.
| HTTP | Code | Retry | What to do |
|---|---|---|---|
| 400 | invalid_request | No | Fix the request; error.details.errors[] lists bad params. Also returned as 404 (unknown task id), 409, 413, 415. |
| 401 | unauthorized | No | Check the key (missing, wrong, revoked or expired). |
| 402 | insufficient_balance | No | Redeem a code, then submit again. |
| 402 | quota_exceeded | No | Raise the key budget or account limit, or wait for the window to reset. |
| 403 | forbidden | No | The key lacks the scope (generate to submit, read to poll). |
| 403 | model_not_allowed | No | The key may not use this model; edit its model list. |
| 403 | ip_not_allowed | No | Call from an allowed address (the message names the one we saw). Nothing is charged. |
| 403 | key_config_error | No | The key custom line list has no usable line for this model; fix it in the console. |
| 404 | model_not_found | No | Use an id from the models page or GET /v1/media/models. |
| 422 | content_policy_violation | No | Change the prompt or input media. |
| 429 | rate_limited | Yes, with backoff | Wait for Retry-After, then retry. |
| 500 | server_error | Yes, with backoff | Retry with the same Idempotency-Key. |
| 502 | upstream_error | Yes, with backoff | Retry with backoff; the model failed to produce a result. |
| 503 | channel_unavailable | Yes, with backoff | Retry with backoff. No line is free right now. |
| 504 | timeout | Yes, with backoff | Resubmit; the task took too long. |
// 422 invalid_request
{ "error": { "code": "invalid_request", "message": "Invalid parameters", "retryable": false, "request_id": "req_...",
"details": { "errors": [{ "field": "aspect_ratio", "message": "must be one of 1:1, 16:9" }] } } }
// 402 insufficient_balance
{ "error": { "code": "insufficient_balance", "message": "Not enough credits", "retryable": false, "request_id": "req_..." } }