the maginary api quickstart

· by Maginary Team · 7 min read

what you’ll build

By the end of this post you’ll have:

  • an api key
  • a POST /api/gens/ that kicks off a generation
  • a working GET /api/gens/{uuid}/ poller (and the webhook alternative)
  • a mental model for what comes back and how to render it

If you want the terse machine contract without the prose, jump to the api reference. This post is the walkthrough with reasoning.

1. get an api key

Sign in at app.maginary.ai, open the dashboard, and grab a key from the api keys section.

Each key is a bearer token. Treat it like a password — server-side only, not shipped to the browser. If you do leak it, rotate immediately; the same page also holds your webhook_secret, but that one is per-account and doesn’t rotate when you rotate api keys.

2. your first generation

Every generation is a POST /api/gens/. The only required field is prompt. Everything else — aspect ratio, model choice, video vs. image — rides inside the prompt string as --flag parameters.

curl -X POST https://app.maginary.ai/api/gens/ 
  -H "Authorization: Bearer $MAGINARY_API_KEY" 
  -H "Content-Type: application/json" 
  -d '{
    "prompt": "a cinematic portrait of a cyberpunk samurai --ar 16:9 --flagship"
  }'

You’ll get back a 201 Created with a JSON body carrying the freshly minted uuid, the parsed action_type (here txt2img), and a processing_state that starts at QUEUED or PROCESSING. Save the uuid — that’s how you fetch the result and where webhook events are keyed.

{
  "uuid": "0c8c1f3a-1a2b-4d8e-9f01-1234567890ab",
  "prompt": "a cinematic portrait of a cyberpunk samurai --ar 16:9 --flagship",
  "action_type": "txt2img",
  "processing_state": "QUEUED",
  "expected_output_count": 4,
  "url": "https://app.maginary.ai/api/gens/0c8c1f3a-1a2b-4d8e-9f01-1234567890ab/",
  "created_at": "2026-07-11T12:34:50.112Z"
}

expected_output_count tells you how many slots the model will return. Default is 4 for txt2img, 1 for most other operations. Override with --1 / --2 / --3 / --4 in the prompt.

3. wait for the result

Two ways: webhook (push) or polling (pull). Pick webhook if you already run an HTTPS server; pick polling if you’re prototyping from a laptop or writing a script.

option a — webhook (recommended for production)

Add callback_url to your POST /api/gens/ body. When the generation reaches DONE or FAILED, we POST the full generation JSON back to you once. Signature verification, retry policy, and per-language code samples are in the webhooks guide.

curl -X POST https://app.maginary.ai/api/gens/ 
  -H "Authorization: Bearer $MAGINARY_API_KEY" 
  -H "Content-Type: application/json" 
  -d '{
    "prompt": "neon city skyline --ar 16:9 --flagship",
    "callback_url": "https://your-app.example.com/webhooks/maginary"
  }'

Under the hood we still update the generation record continuously, so a webhook drop doesn’t mean lost data — you can always fall back to option (b).

option b — polling

Hit GET /api/gens/{uuid}/ on a backoff. In production, prefer webhooks; polling is fine for scripts.

curl -H "Authorization: Bearer $MAGINARY_API_KEY" 
  https://app.maginary.ai/api/gens/0c8c1f3a-1a2b-4d8e-9f01-1234567890ab/

processing_state transitions through QUEUEDPROCESSINGDONE or FAILED. For a fresh-in-line txt2img on a warm queue, expect DONE in ~15–45 seconds. Sensible poll cadence: every 3 seconds for the first 30 seconds, then every 10 seconds after that.

4. reading the response

Regardless of how you learn the generation finished, the response body is the same.

{
  "uuid": "0c8c1f3a-...",
  "prompt": "...",
  "action_type": "txt2img",
  "processing_state": "DONE",
  "processing_started_at": "2026-07-11T12:34:51.004Z",
  "processing_ended_at": "2026-07-11T12:35:18.776Z",
  "processing_result": {
    "job_id": "mj_4f1c...",
    "slots": [
      { "index": 0, "status": "success", "url": "https://s.maginary.ai/.../0.png", "charged": true },
      { "index": 1, "status": "success", "url": "https://s.maginary.ai/.../1.png", "charged": true },
      { "index": 2, "status": "success", "url": "https://s.maginary.ai/.../2.png", "charged": true },
      { "index": 3, "status": "success", "url": "https://s.maginary.ai/.../3.png", "charged": true }
    ],
    "error_message": null,
    "available_actions": {
      "0": ["upscale", "vary", "vary_strong", "zoom_out", "pan_left", "pan_right", "pan_up", "pan_down"],
      "1": ["upscale", "vary", "vary_strong", "zoom_out", "pan_left", "pan_right", "pan_up", "pan_down"],
      "2": ["upscale", "vary", "vary_strong", "zoom_out", "pan_left", "pan_right", "pan_up", "pan_down"],
      "3": ["upscale", "vary", "vary_strong", "zoom_out", "pan_left", "pan_right", "pan_up", "pan_down"],
      "global": ["reroll"]
    }
  },
  "image_urls": [
    "https://s.maginary.ai/.../0.png",
    "https://s.maginary.ai/.../1.png",
    "https://s.maginary.ai/.../2.png",
    "https://s.maginary.ai/.../3.png"
  ],
  "expected_output_count": 4,
  "tokens": 3
}

The two fields your client will actually read:

  • image_urls[] — flat list of finished image (or video) URLs. Use this if you just want to render the outputs.
  • processing_result.slots[] — the same thing but with per-slot metadata (status, charged, index). Use this if a slot might fail while others succeed (partial failure is a real state).

The available_actions map tells you what follow-up ops each slot supports (upscale, vary, pan_*, …). Feed those back into POST /api/gens/{uuid}/actions/ to build a UI like the Maginary app itself.

5. code examples

python

import os, time, requests

BASE = "https://app.maginary.ai/api"
KEY  = os.environ["MAGINARY_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}

def generate(prompt: str, callback_url: str | None = None) -> dict:
    body = {"prompt": prompt}
    if callback_url:
        body["callback_url"] = callback_url
    r = requests.post(f"{BASE}/gens/", json=body, headers=HEADERS, timeout=30)
    r.raise_for_status()
    return r.json()

def wait_for(uuid: str, timeout_s: int = 300) -> dict:
    """Poll until DONE / FAILED. Prefer webhooks for production."""
    deadline = time.time() + timeout_s
    delay = 3
    while time.time() < deadline:
        r = requests.get(f"{BASE}/gens/{uuid}/", headers=HEADERS, timeout=15)
        r.raise_for_status()
        gen = r.json()
        if gen["processing_state"] in ("DONE", "FAILED"):
            return gen
        time.sleep(delay)
        delay = min(delay + 2, 10)  # linear backoff, cap at 10s
    raise TimeoutError(f"Generation {uuid} did not finish in {timeout_s}s")

if __name__ == "__main__":
    gen = generate("a fox in autumn foliage --ar 16:9 --flagship")
    result = wait_for(gen["uuid"])
    for url in result["image_urls"]:
        print(url)

node.js

const BASE = "https://app.maginary.ai/api";
const KEY  = process.env.MAGINARY_API_KEY;
const H    = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };

async function generate(prompt, callbackUrl) {
  const body = callbackUrl ? { prompt, callback_url: callbackUrl } : { prompt };
  const r = await fetch(`${BASE}/gens/`, { method: "POST", headers: H, body: JSON.stringify(body) });
  if (!r.ok) throw new Error(`POST /gens/ ${r.status}: ${await r.text()}`);
  return r.json();
}

async function waitFor(uuid, timeoutMs = 300_000) {
  const deadline = Date.now() + timeoutMs;
  let delay = 3_000;
  while (Date.now() < deadline) {
    const r = await fetch(`${BASE}/gens/${uuid}/`, { headers: H });
    if (!r.ok) throw new Error(`GET /gens/${uuid}/ ${r.status}`);
    const gen = await r.json();
    if (gen.processing_state === "DONE" || gen.processing_state === "FAILED") return gen;
    await new Promise((ok) => setTimeout(ok, delay));
    delay = Math.min(delay + 2_000, 10_000);
  }
  throw new Error(`Generation ${uuid} did not finish in ${timeoutMs}ms`);
}

const gen    = await generate("a fox in autumn foliage --ar 16:9 --flagship");
const result = await waitFor(gen.uuid);
for (const url of result.image_urls) console.log(url);

6. errors

auth: a 401 from POST /api/gens/ means the bearer token was wrong or expired. Regenerate it from the dashboard and try again.

bad prompt / bad parameter: a 400 with a body like {"detail": "..."}. The engine parser validates every --flag before charging you — an unknown flag or an out-of-range value fails synchronously. --cref and --cw are recognized but explicitly reserved (see parameters), so you’ll get a “recognized but not yet implemented” error rather than a silent no-op.

credit shortage: a 402 when your account balance can’t cover the estimate. Top up from the pricing page or reduce the --N count.

model failure: the request is accepted (201), but processing_state eventually becomes FAILED. Read processing_result.error_message for the reason and slots[i].status per slot — partial failures happen when some slots come back and others error.

rate limit: a 429 with a Retry-After header. Back off and retry. The limit is generous but exists to protect the queue; if you’re hitting it regularly, the B2B partner tier has a much higher ceiling.

7. what next

  • Parameters. Every --flag the engine accepts, categorized and searchable: /docs/parameters. Same data as JSON at /docs/parameters.json for LLM tool-use.
  • Webhooks. Full setup + HMAC verification: /blog/webhooks-guide.
  • API reference. Full endpoint catalog (OpenAPI + Scalar): /docs/api.
  • Follow-up operations. Once you have a generation, you can upscale / vary / pan / zoom via POST /api/gens/{uuid}/actions/. The available_actions field on each slot tells you what’s legal.
  • Videos. Add --mp4 and a duration flag (--5sec, --10sec) to trigger txt2vid or img2vid. Same endpoint, same response shape — image_urls becomes an mp4 URL.

Happy generating.