# Overview > diffusr makes ad videos, images and music. Call it from code, or let your AI agent do it. This page is the map. ## What you can make | Tool | Makes | | --- | --- | | `make_video` | A video clip from a prompt, with optional pictures and voice clips | | `make_image` | An image from a prompt | | `edit_image` | A changed copy of a picture | | `recast` | The same clip with a different person or place | | `make_music` | A song, about a minute | | `creator_video` | A video of a creator saying your script (gated preview) | | `run_template` | A ready-made recipe as one job: product ad, video template, talking head, podcast | Every tool is a REST route and an MCP tool with the same name, the same JSON and the same results. ## The loop every call follows 1. **Price it, free.** Add `"dry_run": true`. Nothing is made, nothing is spent. The answer has the price in credits (1 credit = $0.01) and holds for an hour. 2. **Start it.** Send the same call without `dry_run`, with an `Idempotency-Key` header so a retry never pays twice. You get a job `id` at once. 3. **Wait or be told.** Poll `GET /results/{id}` every 15 to 30 seconds, or pass `callback_url` and we send one signed event when it ends. 4. **Take the file.** When `status` is `SUCCEEDED`, `output.url` is the file. You are charged when the result is delivered, never more than the price you saw. A failed job costs nothing. **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_video \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "A woman turns to camera on a sunny street, smiles", "model": "seedance-2-0-fast", "resolution": "720p", "duration_sec": 5, "aspect_ratio": "9:16", "dry_run": true }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeVideo({ prompt: "A woman turns to camera on a sunny street, smiles", model: "seedance-2-0-fast", resolution: "720p", duration_sec: 5, aspect_ratio: "9:16", dry_run: true, }); console.log(job); // the price, nothing was made ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_video( prompt="A woman turns to camera on a sunny street, smiles", model="seedance-2-0-fast", resolution="720p", duration_sec=5, aspect_ratio="9:16", dry_run=True, ) print(job) # the price, nothing was made ``` ## Keys - `dfsr_live_...` runs for real and spends credits. - `dfsr_test_...` is free: real prices, fake results, nothing charged. Build with this one. See [Test keys](https://diffusr.ai/docs/test-keys). Make keys on the **Developers** page in the app: https://app.diffusr.ai/developers ## Where to go next - [Quickstart](https://diffusr.ai/docs/quickstart): key, price, start, result, in five minutes. - [Prices](https://diffusr.ai/docs/prices): per second and per image, next to other services. - [Callbacks](https://diffusr.ai/docs/callbacks), [Errors](https://diffusr.ai/docs/errors), [Limits](https://diffusr.ai/docs/limits), [Uploads and media](https://diffusr.ai/docs/uploads), [Templates](https://diffusr.ai/docs/templates). - [All routes](https://diffusr.ai/docs/reference): one page per route. - [MCP and skills](https://diffusr.ai/docs/mcp-and-skills): connect an AI agent. ## For AI agents Every page here has a plain Markdown twin: add `.md` to the address (`/docs/quickstart.md`). The index is [llms.txt](https://diffusr.ai/docs/llms.txt); everything in one file is [llms-full.txt](https://diffusr.ai/docs/llms-full.txt); the OpenAPI 3.1 spec is https://app.diffusr.ai/api/v2/openapi.json. Source: https://diffusr.ai/docs --- # Quickstart > Check the price for free, make the video, and get told when it is ready. Use a test key while you build: results are fake and nothing is charged. ## 1. Get a key Open **Developers** in the app (https://app.diffusr.ai/developers) and make a test key. It starts with `dfsr_test_`. Keep it in an environment variable. Never put it in code you publish. ```bash export DIFFUSR_KEY=dfsr_test_... ``` For TypeScript run `npm install diffusr`. For Python run `pip install diffusr`. cURL needs nothing. ## 2. Check the price, free Add `"dry_run": true` to any call. Nothing is made and nothing is spent. The price holds for an hour. **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_video \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "A woman turns to camera on a sunny street, smiles", "model": "seedance-2-0-fast", "resolution": "720p", "duration_sec": 5, "aspect_ratio": "9:16", "dry_run": true }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeVideo({ prompt: "A woman turns to camera on a sunny street, smiles", model: "seedance-2-0-fast", resolution: "720p", duration_sec: 5, aspect_ratio: "9:16", dry_run: true, }); console.log(job); // the price, nothing was made ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_video( prompt="A woman turns to camera on a sunny street, smiles", model="seedance-2-0-fast", resolution="720p", duration_sec=5, aspect_ratio="9:16", dry_run=True, ) print(job) # the price, nothing was made ``` The answer has `price.credits` (and `credits_estimated`, the same number), `price.usd`, and `price.expires_at`. ## 3. Make it Send the same call without `dry_run`. Always send an `Idempotency-Key`: if the connection drops, repeat the same request with the same key and you get the first job back instead of paying twice. Add a `callback_url` if you would rather be told than ask. **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_video \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "A woman turns to camera on a sunny street, smiles", "model": "seedance-2-0-fast", "resolution": "720p", "duration_sec": 5, "aspect_ratio": "9:16" }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeVideo({ prompt: "A woman turns to camera on a sunny street, smiles", model: "seedance-2-0-fast", resolution: "720p", duration_sec: 5, aspect_ratio: "9:16", }); const result = await d.waitFor(job.id); console.log(result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_video( prompt="A woman turns to camera on a sunny street, smiles", model="seedance-2-0-fast", resolution="720p", duration_sec=5, aspect_ratio="9:16", ) result = d.wait_for(job["id"]) print(result["output"]["url"]) ``` > You are charged when the video is delivered, never more than the price you saw. A failed video costs nothing. The answer is `202` with an `id`. The work runs in the background. ## 4. Get the video Ask for the result by its id every 15 to 30 seconds until `status` is `SUCCEEDED`, `FAILED` or `CANCELED`. Or wait for the signed [callback](https://diffusr.ai/docs/callbacks). **cURL** ```bash curl "https://app.diffusr.ai/api/v2/results/run_8Hk2xxxxxxxx" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const result = await d.getResult("run_8Hk2xxxxxxxx"); console.log(result.status, result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY result = d.get_result("run_8Hk2xxxxxxxx") print(result["status"], result.get("output")) ``` When it succeeds, `output.url` is the file and `charge` says what was charged. Videos take 1 to 10 minutes, images 10 to 30 seconds. ## Rules an agent should follow - Dry run first, show the user the price, and ask before spending. - One Idempotency-Key per intended job. A new key is a new job and a new charge. - Read `code` in an error, not the message. See [Errors](https://diffusr.ai/docs/errors). - Stop when you get `INSUFFICIENT_CREDITS` or `BUDGET_EXCEEDED` and tell the user. Next: [Test keys](https://diffusr.ai/docs/test-keys), [Prices](https://diffusr.ai/docs/prices), [Callbacks](https://diffusr.ai/docs/callbacks). Source: https://diffusr.ai/docs/quickstart --- # Test keys > A test key does everything a live key does except spend. Prices are real, results are placeholders, nothing is charged. ## What a test key does - Starts with `dfsr_test_`. A live key starts with `dfsr_live_`. - Quotes the exact price, the same as a live key. Dry runs are identical. - Holds and charges nothing. No model runs. - Returns a placeholder file a few seconds later. Results say `"test": true`. - Has its own results, callback log and callback signing secret. A test key never sees live results and a live key never sees test results (a wrong-mode id answers `NOT_FOUND`). **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_image \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "A can of sparkling water on a wet street, soft morning light", "aspect_ratio": "1:1" }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeImage({ prompt: "A can of sparkling water on a wet street, soft morning light", aspect_ratio: "1:1", }); const result = await d.waitFor(job.id); console.log(result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_image( prompt="A can of sparkling water on a wet street, soft morning light", aspect_ratio="1:1", ) result = d.wait_for(job["id"]) print(result["output"]["url"]) ``` ## What a test key cannot do - Use ads routes, or video templates (`run_template` with a video template). These answer `TEST_MODE_UNSUPPORTED` (403). - Run more than a fixed number of test runs a day: see [Limits](https://diffusr.ai/docs/limits) (`TEST_LIMIT`). A test key can upload files. Test uploads are kept for a shorter time: see [Uploads and media](https://diffusr.ai/docs/uploads). ## Moving to live Change nothing but the key. Run a dry run, show the price to the person, and start with a live key. Source: https://diffusr.ai/docs/test-keys --- # Prices > What one second of video or one image costs through the diffusr API, next to what fal and other API sellers charge at the same setting. 1 credit = $0.01. The diffusr column is our API price at the setting shown, read from the live model list when these docs were built. The other columns are the other services' own published list prices at the same setting, checked on the date below, with every condition named under the table. Vendor prices change; check before you decide. | Model | At this setting | diffusr | fal | Higgsfield | | --- | --- | --- | --- | --- | | `gpt-image-2` (GPT Image 2) | 1024x1024, medium quality, 1:1 | **6 cr/image** ($0.06/image) | [$0.053/image](https://fal.ai/models/openai/gpt-image-2) | Not offered | | `gpt-image-2-5-flare` (GPT Image 2.5 Flare) | 1024x1024, medium quality, 1:1 | **2 cr/image** ($0.02/image) | [$0.01317/image](https://fal.ai/models/openai/gpt-image-2.5/flare/text-to-image) | Not offered | | `nano-banana-2` (Nano Banana 2) | 1K, 1:1 | **5 cr/image** ($0.05/image) | [$0.08/image](https://fal.ai/models/fal-ai/nano-banana-2/llms.txt) | Not offered | | `seedance-2-mini` (Seedance 2.0 Mini) | 720p, 16:9, 5s, with audio | **7.78 cr/second** ($0.0778/second) | [$0.1547/second](https://fal.ai/models/bytedance/seedance-2.0/mini/text-to-video/llms.txt) | Not offered | | `seedance-2-0-fast` (Seedance 2.0 Fast) | 720p, 16:9, 5s, with audio | **12.45 cr/second** ($0.1245/second) | [$0.2419/second](https://fal.ai/models/bytedance/seedance-2.0/fast/text-to-video/llms.txt) | Not offered | | `seedance-2-0` (Seedance 2.0) | 720p, 16:9, 5s, with audio | **15.56 cr/second** ($0.1556/second) | [$0.3034/second](https://fal.ai/models/bytedance/seedance-2.0/text-to-video/llms.txt) | [$0.3024/second](https://open.higgsfield.ai/models/bytedance/seedance-2.0/text-to-video/playground) | | `seedance-2-5` (Seedance 2.5) | 720p, 16:9, 5s, with audio | **23.78 cr/second** ($0.2378/second) | [$0.473/second](https://fal.ai/models/bytedance/seedance-2.5/text-to-video/llms.txt) | [$0.4622/second](https://open.higgsfield.ai/models/bytedance/seedance-2.5/text-to-video/playground) | | `gemini-omni-1-1-flash` (Gemini Omni Flash 1.1) | 720p, 16:9, 8s, with audio | **10.20 cr/second** ($0.102/second) | [$0.10/second](https://fal.ai/models/google/gemini-omni-flash/v1.1/text-to-video/llms.txt) | Not offered | | `kling-3-pro` (Kling 3.0 Pro) | 16:9, 5s, with audio | **17.14 cr/second** ($0.1714/second) | [$0.168/second](https://fal.ai/models/fal-ai/kling-video/v3/pro/text-to-video/llms.txt) | Not published | | `kling-4` (Kling 4.0) | 720p, 16:9, 5s, with audio | **16.02 cr/second** ($0.1602/second) | [$0.157/second](https://fal.ai/models/kling/v4/text-to-video/llms.txt) | [$0.157/second](https://open.higgsfield.ai/models/kling-video/v4.0/text-to-video/playground) | | `wan-3` (Wan 3.0) | 720p, 16:9, 5s, with audio | **10.20 cr/second** ($0.102/second) | [$0.10/second](https://fal.ai/models/alibaba/wan-3.0/text-to-video/llms.txt) | [$0.10/second](https://open.higgsfield.ai/models/alibaba/wan-3.0/text-to-video/playground) | | `minimax-h3` (MiniMax H3) | 2K, 16:9, 5s | **13.26 cr/second** ($0.1326/second) | Not published | [$0.13/second](https://open.higgsfield.ai/models/minimax/h3/text-to-video/playground) | Conditions behind the other columns (checked 2026-11-09): - GPT Image 2, fal: Token-priced; medium quality 1024x1024. High quality (fal default) is 0.211. - GPT Image 2, Higgsfield: Not offered: not in the Higgsfield API catalog (open.higgsfield.ai/explore, checked 2026-11-09). - GPT Image 2.5 Flare, fal: Token-priced; medium quality 1024x1024. High (fal default) is 0.05268. - GPT Image 2.5 Flare, Higgsfield: Not offered: not in the Higgsfield API catalog (open.higgsfield.ai/explore, checked 2026-11-09). - Nano Banana 2, fal: 1K; 2K 0.12, 4K 0.16. Web search add-on +0.015. - Nano Banana 2, Higgsfield: Not offered: not in the Higgsfield API catalog (open.higgsfield.ai/explore, checked 2026-11-09). - Seedance 2.0 Mini, fal: Page says 'roughly'; token billed at 0.007 per 1k tokens. Audio costs the same. - Seedance 2.0 Mini, Higgsfield: Not offered: not in the Higgsfield API catalog (open.higgsfield.ai/explore, checked 2026-11-09). - Seedance 2.0 Fast, fal: Audio costs the same. - Seedance 2.0 Fast, Higgsfield: Not offered: not in the Higgsfield API catalog (open.higgsfield.ai/explore, checked 2026-11-09). - Seedance 2.0, fal: Audio costs the same. - Seedance 2.0, Higgsfield: Derived, not printed: page gives 0.014 per 1k tokens, tokens = h*w*sec*24/1024, so 1280x720 = 21,600 tokens/s. List price. Higgsfield launch offer cuts it 30-50% for 7 days; new sign-ups get 15% off; 'pick any three models' unlocks up to 75% (open.higgsfield.ai/pricing, higgsfield.ai/higgsfield-api). - Seedance 2.5, fal: Page says 'roughly'; 0.0214 per 1k tokens (formula gives about 0.462). Audio costs the same. Reference-to-video with a video input is x0.6. - Seedance 2.5, Higgsfield: 16:9, no video input. List price. Higgsfield launch offer cuts it 30-50% for 7 days; new sign-ups get 15% off; 'pick any three models' unlocks up to 75% (open.higgsfield.ai/pricing, higgsfield.ai/higgsfield-api). - Gemini Omni Flash 1.1, fal: 8 s default; 1080p 0.15, 4K 0.30. - Gemini Omni Flash 1.1, Higgsfield: Not offered: not in the Higgsfield API catalog (open.higgsfield.ai/explore, checked 2026-11-09). - Kling 3.0 Pro, fal: Audio on. Audio off is 0.112. - Kling 3.0 Pro, Higgsfield: Unknown: only a 'from' list price of 0.084/s (launch offer 0.042) is published; the audio-on rate at this setting is not shown on the public pages. - Kling 4.0, fal: 720p; 1080p 0.235. Page does not say whether audio changes the price. - Kling 4.0, Higgsfield: Lowest list rate shown ('from' 0.157, 0.1099 after the 30% launch discount); assumed to be 720p. Audio state not confirmed; page says price varies by resolution and audio. List price. Higgsfield launch offer cuts it 30-50% for 7 days; new sign-ups get 15% off; 'pick any three models' unlocks up to 75% (open.higgsfield.ai/pricing, higgsfield.ai/higgsfield-api). - Wan 3.0, fal: 720p; 480p 0.05, 1080p 0.20. No separate audio charge listed. - Wan 3.0, Higgsfield: 720p list; the same page also shows 0.025-0.10 across 480p-1080p, which conflicts. List price. Higgsfield launch offer cuts it 30-50% for 7 days; new sign-ups get 15% off; 'pick any three models' unlocks up to 75% (open.higgsfield.ai/pricing, higgsfield.ai/higgsfield-api). - MiniMax H3, fal: fal has no 2K tier (480P/768P/1080P). Nearest is 1080P: 0.112 now (30% promo, Nov 1-15 2026), 0.16 standard from Nov 16. - MiniMax H3, Higgsfield: 2K list 0.13 per second; 0.065 with the 50% launch discount. List price. Higgsfield launch offer cuts it 30-50% for 7 days; new sign-ups get 15% off; 'pick any three models' unlocks up to 75% (open.higgsfield.ai/pricing, higgsfield.ai/higgsfield-api). ## How the numbers work - A video price is per second at that resolution and length. The charge for a clip is whole credits. - An image price is per image. - Prices on these pages are the model rate. Tools that shape a result for you (`recast`, `creator_video`, `run_template`) run at the **outcome rate**, which is higher than the model rate. Use `dry_run: true` for the exact price of any call. - `GET /api/v2/models` returns the same prices live, without a key: [reference](https://diffusr.ai/docs/reference/list_models). - You are charged when the result is delivered and never more than the quoted price. Failed work is free. - Test keys quote the same prices and charge nothing. Source: https://diffusr.ai/docs/prices --- # Callbacks > Pass a callback_url and we send one signed event when your job ends. Verify the signature, answer 2xx, ignore repeats. ## Turn it on Add `callback_url` to any make call. It must be a public `https://` address with no username or password. A bad address answers `422 CALLBACK_URL_INVALID` before anything is charged. Polling `GET /results/{id}` keeps working too. **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_video \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "A woman turns to camera on a sunny street, smiles", "duration_sec": 5, "callback_url": "https://example.com/diffusr-hook" }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeVideo({ prompt: "A woman turns to camera on a sunny street, smiles", duration_sec: 5, callback_url: "https://example.com/diffusr-hook", }); const result = await d.waitFor(job.id); console.log(result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_video( prompt="A woman turns to camera on a sunny street, smiles", duration_sec=5, callback_url="https://example.com/diffusr-hook", ) result = d.wait_for(job["id"]) print(result["output"]["url"]) ``` ## What we send One `POST` when the job ends (succeeded, failed or canceled): ```json { "id": "evt_...", "type": "result.finished", "created_at": "2026-11-09T00:20:00Z", "data": { "id": "run_...", "status": "SUCCEEDED", "output": { "kind": "VIDEO", "url": "https://..." } } } ``` `data` is the same object `GET /results/{id}` returns. ## Check the signature Callbacks follow [Standard Webhooks](https://www.standardwebhooks.com). Three headers come with each one: - `webhook-id`: the event id. It is the same on every retry. Remember it and ignore repeats. - `webhook-timestamp`: unix seconds when this try was signed. Reject anything more than 5 minutes off. - `webhook-signature`: `v1,`, the HMAC-SHA256 of `".."`, keyed with the bytes of the base64 part of your `whsec_...` secret. After a secret change it carries two signatures separated by a space for 24 hours: accept either. Use the **raw** body, not a parsed and re-written one. Compare in constant time. Get your secret with `GET /callbacks/secret` (it is made the first time you ask). Live keys and test keys have separate secrets. `POST /callbacks/secret/rotate` makes a new one; the old one keeps signing for 24 hours. The SDKs do all of this for you: ```ts // Express: use the RAW body, not the parsed one. app.post("/diffusr", express.text({ type: "*/*" }), (req, res) => { if (!Diffusr.verifyCallback(req.body, req.headers, process.env.DIFFUSR_WHSEC!)) { return res.sendStatus(400); } const event = JSON.parse(req.body); // { id, type: "result.finished", data: } res.sendStatus(200); // any 2xx; drop repeats of event.id }); ``` ```python from diffusr import Diffusr # Flask: use the RAW body, not the parsed one. @app.post("/diffusr") def hook(): raw = request.get_data(as_text=True) if not Diffusr.verify_callback(raw, request.headers, os.environ["DIFFUSR_WHSEC"]): return "", 400 event = json.loads(raw) # {id, type: "result.finished", data: } return "", 200 # any 2xx; drop repeats of event["id"] ``` ## Retries Answer any `2xx` to say it arrived. A `410` stops all further tries. Anything else, a timeout, or a redirect (we do not follow redirects) counts as a failure and is retried. - **Try 1** is sent when the job ends. If it fails, tries 2 to 6 follow after 1 min, 5 min, 30 min, 2 h, 8 h. That is 6 tries in all. - Each try waits up to 10 seconds for your answer. - After the last try the event is marked FAILED. You can retry a FAILED event by hand up to 3 times (`POST /callbacks/{event_id}/retry`). - `callback_url` can be up to 2048 characters. ## The log `GET /callbacks` lists your events, newest first: event id, result id, URL, status, number of tries, last HTTP status, a short last error, next try time. `POST /callbacks/{event_id}/retry` gives a FAILED event one more try now. The Developers page in the app shows the same log. Callbacks are for convenience. If one never arrives, `GET /results/{id}` still has the answer. See also: [get_callback_secret](https://diffusr.ai/docs/reference/get_callback_secret), [list_callbacks](https://diffusr.ai/docs/reference/list_callbacks), [retry_callback](https://diffusr.ai/docs/reference/retry_callback). Source: https://diffusr.ai/docs/callbacks --- # Errors > Every error has a stable code. Branch on the code, not the message. This table is built from the API's own code list. ## The shape Every error body is JSON: ```json { "error": "Not enough credits.", "code": "INSUFFICIENT_CREDITS", "request_id": "req_...", "needed": 63, "balance": 12 } ``` - `code` is stable and machine-readable. Branch on it. Accept a code you do not know and treat it by its HTTP status. - `request_id` is also the `X-Request-Id` header. Quote it when you ask diffusr for help. Send your own `X-Request-Id` (1 to 64 letters, digits, `_` or `-`) to choose it. - Some codes add detail: `issues` (VALIDATION_FAILED, PREFLIGHT_FAILED), `needed` and `balance`, `budget`, `scope`, `limit`, `retry_after_seconds`, `field`, `index`. - `429` and `503` answers carry a `Retry-After` header: seconds to wait. - A refused request is never charged. - MCP tool errors start with `[CODE]` and end with a `request_id:` line. ## Every code | Code | HTTP | What it means | Retry? | What to do | | --- | --- | --- | --- | --- | | `UNAUTHORIZED` | 401 | Missing, malformed or unknown API key. | No | Send `Authorization: Bearer `. Copy the key again from Developers in the app. | | `KEY_REVOKED` | 401 | This key was revoked. Make a new one on the Developers page. | No | Make a new key on the Developers page and use that. | | `KEY_EXPIRED` | 401 | This key passed its expiry date. Make a new one. | No | Make a new key on the Developers page. | | `SCOPE_DENIED` | 403 | The key does not have the scope this call needs. | No | Use a key that has the scope the route names, or make a new key with it. | | `MODEL_NOT_ALLOWED` | 403 | The key is limited to other models. | No | Pass a `model` the key may use (the default model counts), or use another key. | | `TEST_MODE_UNSUPPORTED` | 403 | This call is not available with a test key. | No | Use a live key for this call. | | `ACCOUNT_BLOCKED` | 403 | The account is blocked. Contact support. | No | Stop. Tell the user to contact diffusr. | | `FORBIDDEN` | 403 | Not allowed (a refusal that has no more specific code). | No | Read `error` for the reason. Do not retry as is. | | `VALIDATION_FAILED` | 422 | The input did not match the schema; `issues` lists each field and why. Nothing was made, nothing charged. | No | Read `issues`: each has a field path and a reason. Fix those fields and send again. Nothing was charged. | | `INVALID_JSON` | 422 | The body was not a JSON object. | No | Send a JSON object with `Content-Type: application/json`. | | `INVALID_REQUEST` | 400 or 422 | The request was understood but cannot be done as sent (a refusal that has no more specific code). | No | Read `error`, change the request, send again. | | `PREFLIGHT_FAILED` | 422 | The job cannot run as described (for example a missing input); `issues` lists why. Nothing was charged. | No | Read `issues` for what is missing or unusable, fix it, send again. Nothing was charged. | | `CALLBACK_URL_INVALID` | 422 | callback_url is not usable: it must be a public https address with no password. | No | Use a public `https://` address with no username or password. | | `CALLBACK_NOT_RETRYABLE` | 409 | That callback event can't be retried now: it was delivered, is still being tried, or is out of manual retries. | No | Check the event in GET /callbacks. Only a FAILED event with manual retries left can be retried. | | `REFERENCE_UNREACHABLE` | 422 | A reference file could not be fetched. | No | Make the link public and reachable without a login, or upload the file with POST /uploads. | | `REFERENCE_TOO_LARGE` | 422 | A reference file is over the model's size or length limit. | No | Send a smaller or shorter file. Limits are on the Uploads and media page. | | `REFERENCE_UNSUPPORTED` | 422 | A reference file is the wrong kind for this model. | No | Send the right kind of file: a picture for `image_urls`, audio for `audio_urls`, video for `video_url`. | | `REFERENCE_FORBIDDEN` | 403 | A reference points at a file the caller may not use. | No | Use a link to your own upload or result, or any public link. Do not use `asset://` links. | | `NOT_FOUND` | 404 | No such id, or it belongs to someone else (or to the other key mode: test vs live). | No | Check the id. A test key cannot see live results, and a live key cannot see test results. | | `IDEMPOTENCY_CONFLICT` | 409 | This Idempotency-Key was already used for a different request. | No | Use a new Idempotency-Key for a different request. Reuse the old key only to repeat the exact same request. | | `CONFLICT` | 409 | The request conflicts with the current state (a refusal that has no more specific code). | No | Read `error`, fetch the current state, then decide. | | `INSUFFICIENT_CREDITS` | 402 | Not enough free credits; `needed` and `balance` say by how much. | No | Tell the user. `needed` and `balance` say how short they are. They can top up in the app. | | `allowance_exceeded` | 402 | A team member's limit or the team pot is used up (legacy lowercase code, kept for existing clients). | No | Tell the user. A team limit is used up. | | `BUDGET_EXCEEDED` | 402 | The key's or account's daily or monthly API budget would be passed; `budget` has the window and when it frees up. | No | Tell the user. `budget.resets_at` says when the window frees up. Do not retry before then. | | `RATE_LIMITED` | 429 | Too many requests of this kind this minute. | After `Retry-After` | Wait `Retry-After` seconds, then send the same request again (same Idempotency-Key). | | `CONCURRENCY_LIMIT` | 429 | Too many jobs queued or running for this key or account. | After `Retry-After` | Wait `Retry-After` seconds, or wait for a running job to finish. The request was refused, not queued. | | `PROVIDER_BUSY` | 503 | Our capacity at the model's supplier is full. Not the caller's fault; try again. | After `Retry-After` | Wait `Retry-After` seconds, then retry. Not your fault; nothing was charged. | | `AUTH_RATE_LIMITED` | 429 | Too many failed sign-in attempts from this address. | After `Retry-After` | Stop sending bad keys. Wait `Retry-After` seconds. | | `KEY_LIMIT` | 429 | The key hit its own limit (for example how many keys an account may hold). | After `Retry-After` | Revoke keys you do not use, or wait `Retry-After` seconds before making more. | | `TEST_LIMIT` | 429 | A test key hit its own cap (per minute or per day). Wait, or use a live key. | After `Retry-After` | Wait `Retry-After` seconds, or use a live key. | | `UPLOAD_QUOTA` | 429 | Too many uploads in the last 24 hours. | After `Retry-After` | Wait `Retry-After` seconds. Reuse a `file_url` you already have instead of uploading again. | | `STORAGE_QUOTA` | 413 | The account's stored upload bytes are at the cap; uploads clear on their own. | After `Retry-After` | Wait: uploads clear on their own after their retention time. | | `TOO_LARGE` | 413 | The request or file is too large (a refusal that has no more specific code). | No | Send a smaller request or file. | | `WALLET_BUSY` | 503 | The wallet was locked by another request. Retry at once. | Yes, at once | Retry at once with the same Idempotency-Key (`Retry-After` is 2 seconds). | | `API_PAUSED` | 503 | New API starts are paused for everyone. Runs already started finish. | After `Retry-After` | New starts are paused. Wait `Retry-After` seconds. Dry runs still answer. | | `MODEL_PAUSED` | 503 | This model is paused for new API starts. | After `Retry-After` | Use another model, or wait `Retry-After` seconds. | | `PROVIDER_PAUSED` | 503 | This model's supplier is paused for new API starts. | After `Retry-After` | Use another model, or wait `Retry-After` seconds. | | `UNAVAILABLE` | 503 | A service we depend on is not available right now (a refusal that has no more specific code). Retry later. | After `Retry-After` | Wait `Retry-After` seconds if present (else a minute), then retry with the same Idempotency-Key. | | `INTERNAL_ERROR` | 500 | Something went wrong on our side. Quote the request_id to support. | After `Retry-After` | Retry once with the same Idempotency-Key. If it keeps failing, quote `request_id` to diffusr. | ## Retrying safely Retry only the codes marked "After `Retry-After`" or "Yes, at once", and always with the **same** Idempotency-Key. If the first try had already been accepted you get that job back (`idempotent_replay: true`) and are not charged twice. Source: https://diffusr.ai/docs/errors --- # Limits > How many requests and jobs you can have at once. A request over a limit is refused with a Retry-After, never queued, never charged. ## Request and job limits Requests per minute, in fixed 60-second windows. Each is counted for the key and for every key on the same paying account together. | Class | What counts | Per key | Per account | | --- | --- | --- | --- | | `start` | A paid start (`make_video`, `make_image`, `edit_image`, `recast`, `make_music`, `creator_video`, `run_template`) | 10 | 30 | | `quote` | A `dry_run` call, and `GET /models` with a key | 60 | 180 | | `read` | `GET /results`, `GET /results/{id}`, `GET /templates`, the callback log | 120 | 360 | | `upload` | `POST /uploads` | 30 | 60 | | `write` | Drafting an ads launch | 20 | 40 | - **Jobs at once:** up to 8 jobs queued or running per paying account. A key can carry a lower limit of its own. Test-key runs do not count. - **Test keys:** up to 200 test runs per person per 24 hours (`TEST_LIMIT`). Over a request limit the answer is `429 RATE_LIMITED` with `Retry-After` (seconds to the end of the window). A refused request still counts, so waiting works better than hammering. Over the jobs limit the answer is `429 CONCURRENCY_LIMIT` with `Retry-After: 15`. Work you already started keeps running. If our own capacity is full the answer is `503 PROVIDER_BUSY`. That is not your fault and nothing is charged; retry after `Retry-After`. ## Money limits You can cap spend per key and per account, per day and per month, on the Developers page. Over a cap the answer is `402 BUDGET_EXCEEDED` with `budget.resets_at`. It never triggers an automatic top-up. ## Keys At most 25 active keys per person, and 20 new keys an hour (`KEY_LIMIT`). Too many bad keys from one address in a minute answers `AUTH_RATE_LIMITED`. ## How to read your limits in code Poll politely: every 15 to 30 seconds for a job, or use a [callback](https://diffusr.ai/docs/callbacks). Dry runs have their own, larger limit (`quote`), so check prices as often as you need. ## Asking for more If you need higher limits, [book a call](https://cal.com/sharan-mansukhani-hfdv4n/diffusr?overlayCalendar=true) or write to hello@diffusr.ai. Say your use, how many jobs at once, and how many requests a minute. Source: https://diffusr.ai/docs/limits --- # Uploads and media > How to give a model a picture, a clip or a voice. Public links work directly; a local file goes up in two steps. ## Two ways in 1. **A public link.** Put it straight in `image_urls`, `audio_urls` or `video_url`. It must open without a login and answer a ranged request with a size. 2. **A local file.** Ask for a signed upload, then send the bytes. An `asset://` link is refused (`REFERENCE_FORBIDDEN`). Our own media links work only if the file is yours or a teammate's. ## Upload a local file Step 1: say what you will send. The size must be exact. **cURL** ```bash curl -X POST "https://app.diffusr.ai/api/v2/uploads" \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -d '{ "content_type": "video/mp4", "size_bytes": 12345678 }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY // Copies a public link in. For a local file use d.uploadFile("./clip.mp4"). const up = await d.uploadFromUrl("https://example.com/clip.mp4"); console.log(up.file_url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY # Copies a public link in. For a local file use d.upload_file("./clip.mp4"). up = d.upload_from_url("https://example.com/clip.mp4") print(up["file_url"]) ``` The answer has `upload_url`, `file_url` and the headers to use. Step 2: `PUT` exactly those bytes, with the same `Content-Type`, to `upload_url` within 15 minutes: ```bash curl -X PUT -H "Content-Type: video/mp4" --data-binary @clip.mp4 "UPLOAD_URL" ``` Step 3: use `file_url` in your call. A signed upload that was never sent is marked expired after about 20 minutes. The SDKs do all three steps in `uploadFile` / `upload_file`. If the file is already online, pass `source_url` instead and we copy it. For a large video, download it and use the signed upload. ## What each file must be - The right kind: a picture for `image_urls`, audio for `audio_urls`, video for `video_url` (we check the file's first bytes, not just its name). - Within limits: 25 MB a picture, 250 MB a video, 50 MB a sound, 300 MB together. - Seedance models also need pictures up to 6000 px a side with width divided by height between 0.4 and 2.5. Some models limit clip or voice length; the length must be readable (mp4, mov, m4a, wav or mp3). `recast` reads the clip's real length and prices by it, so send mp4 or mov. - A dry run only checks the shape. The file itself is checked when you start the real job, before anything is reserved. Refusals are `422` (or `403` for access rules) with `code`, `field` and `index`: `REFERENCE_UNREACHABLE`, `REFERENCE_TOO_LARGE`, `REFERENCE_UNSUPPORTED`, `REFERENCE_FORBIDDEN`. ## Quotas and how long files stay | | Live keys | Test keys | | --- | --- | --- | | Uploads per 24 hours, per account | 500 | 50 | | Stored at once | 10 GB | 500 MB | | Kept after the last time a job used the file (or after upload, if never used) | 7 days | 1 day | Largest file: 25 MB for a picture, 250 MB for a video, 50 MB for a sound. Over the count: `429 UPLOAD_QUOTA`. Over the storage: `413 STORAGE_QUOTA`; uploads clear on their own. Only API uploads are cleaned up. Your results are not uploads: they stay like the app's. See [upload_asset](https://diffusr.ai/docs/reference/upload_asset). Source: https://diffusr.ai/docs/uploads --- # Templates > run_template makes a whole ad, template video, talking head or podcast as one job. List what is runnable, price it, run it. ## The three calls 1. `GET /templates` (`list_templates` in MCP) lists what you can run, the inputs each takes, and an approximate price. 2. `POST /run_template` with `dry_run: true` gives the exact price. The price in the list is only a hint. 3. The same call without `dry_run` starts it. It is **one job**: poll `GET /results/{id}` or use a [callback](https://diffusr.ai/docs/callbacks), like any other tool. **cURL** ```bash curl "https://app.diffusr.ai/api/v2/templates" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const { templates } = await d.listTemplates(); console.log(templates.map((t) => t.id)); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY data = d.list_templates() print([t["id"] for t in data["templates"]]) ``` ## What can be run | Kind | What it makes | What it needs | | --- | --- | --- | | Product ad | A 20 to 30 second ad from a product photo | `template`, `product_image_url` (your own file from `upload_asset`); `prompt` optional | | Video template (Remix) | A new video in the style of a published template | `template`; `pictures` for its slots; `prompt` optional | | Video template (Recast) | The template's moves with your people in them | `template`, `mode: "recast"`, `image_urls`, `prompt` (what to change) | | Talking head | One person saying your script to camera, one take | `template`, `script`, `place`, `people` (one); `setup` optional | | Podcast | Two hosts in a studio, one take | `template`, `script`, `people` (two) | Talking head and podcast are a single take of up to 30 seconds (15 seconds for the "propped" setup). Everything the script says must fit. A field a template does not take is refused by name, and nothing is charged. `GET /templates` shows each template's inputs; a Remix picture slot appears as `pictures.`. ## Fields Only `template` is required for every call. The full list, with types, is on the [run_template](https://diffusr.ai/docs/reference/run_template) page; in short: - `template`: the id from `GET /templates`. - `mode`: `"remix"` (default) or `"recast"`. Video templates only. - `prompt`: product ad, an optional change ("make it a winter sale"). Remix, your version of the template's prompt (leave out to use the template's own). Recast, what to change. - `pictures`: Remix. One picture per slot, keyed by slot name. - `image_urls`: Recast. Photos of who should appear. - `product_image_url`: product ad (required) or talking head (optional, shows the product on camera). - `script`: talking head, the words exactly as said. Podcast, one line per turn written `Name: what they say`, names exactly as in `people`. - `place`: talking head. Where they are, in a few words. - `people`: `[{ name, image_url, look? }]`. One for a talking head, two for a podcast (the first sits on the left). - `setup`: plain talking head only. `"selfie"` (default), `"propped"` or `"friend"`. - `resolution`: `"480p"` or `"720p"`. 480p costs less. - `dry_run`, `idempotency_key`, `callback_url`: as on every make call. There is no `duration_sec`: the template or the script sets the length. **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/run_template \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "template": "tpl-talking-head", "script": "I tried this for a week and I am not going back.", "place": "a small Paris hotel room with a tall window", "people": [ { "name": "Mia", "image_url": "https://example.com/mia.jpg" } ], "dry_run": true }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.runTemplate({ template: "tpl-talking-head", script: "I tried this for a week and I am not going back.", place: "a small Paris hotel room with a tall window", people: [ { name: "Mia", image_url: "https://example.com/mia.jpg", }, ], dry_run: true, }); console.log(job); // the price, nothing was made ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.run_template( template="tpl-talking-head", script="I tried this for a week and I am not going back.", place="a small Paris hotel room with a tall window", people=[ { "name": "Mia", "image_url": "https://example.com/mia.jpg", }, ], dry_run=True, ) print(job) # the price, nothing was made ``` ## Price `run_template` runs at the **outcome rate**, which is higher than the per-model rates on [Prices](https://diffusr.ai/docs/prices), because we plan and run the whole recipe for you. Always `dry_run` first. ## Other looks Animation looks (claymation, LEGO and others) and the multi-take talking-head chain are not `run_template` kinds. They are recipes you follow with the direct tools: with MCP, each is a prompt and a resource (`diffusr://templates/...`) that lists the exact `make_image` and `make_video` calls. See [MCP and skills](https://diffusr.ai/docs/mcp-and-skills). Test keys cannot run video templates (`TEST_MODE_UNSUPPORTED`). Source: https://diffusr.ai/docs/templates --- # Usage and data > What we keep, how long, how to delete it, and what you may and may not make. The binding terms are on diffusr.ai. The legal terms are the ones on diffusr.ai: [Terms](https://diffusr.ai/terms) and [Privacy](https://diffusr.ai/privacy). This page is the plain summary for developers and does not replace them. ## What we keep - **Your results** (the videos, images and songs you made) stay in your account, like results made in the app. `output.url` stays valid while the result is kept. - **Files you upload through the API** are kept for a set time after you last used them, then deleted. See [Uploads and media](https://diffusr.ai/docs/uploads) for the times. - **Requests**: each call is recorded with its request id, the tool, the model, the price and what was charged, so you can see it in the Developers page and we can help you with a problem. Keys are stored only as a one-way hash; the full key is shown once, when you make it. - **Callback events** and their delivery tries are kept in the callback log. ## Deleting - Revoke a key at any time on the Developers page. It stops working at once. - Delete uploads by letting them expire, or ask us to remove them sooner. - To delete results or your account, use the app, or write to hello@diffusr.ai. ## What you may make Do not use diffusr to make illegal content, content that sexualizes minors, deceptive impersonation of real people, or content that infringes someone's rights. Do not upload photos of people without the right to use them. Some requests are refused automatically. Accounts that break the rules are blocked (`ACCOUNT_BLOCKED`). ## Your key is your account Anything done with your key is charged to you. Keep it out of code you publish and out of the browser. If it leaks, revoke it and make a new one. Set a daily or monthly budget on the key to cap the damage. Source: https://diffusr.ai/docs/usage-and-data --- # SDKs > Two small clients with no dependencies. They send an Idempotency-Key for you, retry safely, wait for results and check callback signatures. ## Install ```bash npm install diffusr # Node 18 or newer pip install diffusr # Python 3.9 or newer ``` Both read the key from `DIFFUSR_KEY`. Bodies and answers are the API's own snake_case JSON, passed through unchanged. (Both are published after launch; until then the cURL form of every call on these docs works today.) ## The whole loop **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_video \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "A woman turns to camera on a sunny street, smiles", "duration_sec": 5, "dry_run": true }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeVideo({ prompt: "A woman turns to camera on a sunny street, smiles", duration_sec: 5, dry_run: true, }); console.log(job); // the price, nothing was made ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_video( prompt="A woman turns to camera on a sunny street, smiles", duration_sec=5, dry_run=True, ) print(job) # the price, nothing was made ``` **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_video \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "A woman turns to camera on a sunny street, smiles", "duration_sec": 5 }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeVideo({ prompt: "A woman turns to camera on a sunny street, smiles", duration_sec: 5, }); const result = await d.waitFor(job.id); console.log(result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_video( prompt="A woman turns to camera on a sunny street, smiles", duration_sec=5, ) result = d.wait_for(job["id"]) print(result["output"]["url"]) ``` `waitFor` / `wait_for` polls with backoff and gives up with `WAIT_TIMEOUT` if the job takes too long. ## Methods | TypeScript | Python | REST | | --- | --- | --- | | `makeVideo`, `makeImage`, `editImage`, `recast`, `makeMusic`, `creatorVideo`, `runTemplate` `(body, { idempotencyKey? })` | `make_video`, `make_image`, `edit_image`, `recast`, `make_music`, `creator_video`, `run_template` `(**body)` | `POST /{tool}` | | `getResult(id)`, `listResults({ limit? })`, `cancel(id)` (stops queued work; a step already generating may finish and be charged: check `charge`) | `get_result`, `list_results`, `cancel` | `GET /results/{id}`, `GET /results`, `POST /results/{id}/cancel` | | `waitFor(id, { timeoutMs?, intervalMs? })` | `wait_for(id, timeout=, interval=)` | polls `GET /results/{id}` | | `uploadFile(...)`, `uploadFromUrl(url)` | `upload_file(...)`, `upload_from_url(url)` | `POST /uploads` + signed `PUT` | | `listModels({ kind? })`, `listTemplates()` | `list_models`, `list_templates` | `GET /models`, `GET /templates` | | `listCallbacks`, `retryCallback`, `getCallbackSecret`, `rotateCallbackSecret` | `list_callbacks`, `retry_callback`, `get_callback_secret`, `rotate_callback_secret` | `/callbacks...` | | `Diffusr.verifyCallback(rawBody, headers, secret)` | `Diffusr.verify_callback(raw_body, headers, secret)` | | `dry_run: true` on any make call answers with the price only. ## Retries and errors - Every request times out after 60 s (`TIMEOUT`). `GET` calls (and `waitFor`) retry on `429`, `500`, `502`, `503`, `504`, network errors and timeouts with backoff, and honour `Retry-After`; make calls retry on `429`, `503`, network errors and timeouts, with the same `Idempotency-Key`. A wait over 30 seconds is not slept: the error is thrown. - Make calls retry the same way **with the same Idempotency-Key**. Pass your own to make a retry safe across restarts. - Every failure is a `DiffusrError` with `status`, `code`, `message`, `requestId` and `retryAfter`. The SDK adds the codes `NETWORK_ERROR`, `WAIT_TIMEOUT`, `UPLOAD_FAILED`, `CONTENT_TYPE_REQUIRED` and `MISSING_API_KEY`. Handle codes you do not know. ```ts try { await d.makeVideo({ prompt: "..." }); } catch (e) { if (e instanceof DiffusrError) console.log(e.status, e.code, e.requestId, e.retryAfter); } ``` ```python from diffusr import DiffusrError try: d.make_video(prompt="...") except DiffusrError as e: print(e.status, e.code, e.request_id, e.retry_after) ``` Verifying callbacks: see [Callbacks](https://diffusr.ai/docs/callbacks). Source: https://diffusr.ai/docs/sdks --- # MCP and skills > Connect the diffusr MCP server, and install the skills that teach an agent to price first and ask before spending. ## Connect the MCP server The server is at `https://app.diffusr.ai/api/mcp`. It signs in with OAuth when you add it by URL, or accepts an API key as `Authorization: Bearer `. A test key works too, and costs nothing. ```bash claude mcp add --transport http diffusr https://app.diffusr.ai/api/mcp ``` Other clients: add a remote MCP server with that URL. The tools have the same names and the same JSON as the REST routes: `make_video`, `make_image`, `edit_image`, `recast`, `make_music`, `creator_video`, `run_template`, `get_result`, `list_results`, `upload_asset`, `list_models`, `list_templates`. Every one that makes something takes `dry_run: true`. Video templates (animation looks, UGC talking head, podcast) are also MCP prompts and resources (`diffusr://templates/...`): step-by-step recipes that use the tools above. ## Skills The agent kit teaches an agent to do it the right way: price first, ask before spending, pick the right tool. | Skill | Use it to | | --- | --- | | `diffusr` | Connect, test vs live keys, dry run and price first, polling vs callbacks, uploads, errors, `run_template` vs recipes | | `ugc-talking-head` | One or two people saying a script to a phone camera | | `two-host-podcast` | Two hosts in a studio, one take up to 30 s | | `product-ad` | A 20 to 30 s UGC product ad from a product photo | | `recast` | The same clip, a new cast or place | | `animation-looks` | A film in an animated look (claymation, LEGO, ...) | | `seedance-references` | Make and pass reference pictures and voice clips | Install: - **Claude Code plugin:** after launch, add the diffusr plugin from its marketplace; it adds the skills and the MCP server together. Until then, add the MCP server as above and copy the skills. - **Codex and other AGENTS.md readers:** copy `AGENTS.md` into your project and the `skills/` folders where your agent reads skills. - **Cursor:** copy `diffusr.mdc` into `.cursor/rules/` and add the MCP server URL in Cursor's MCP settings. - **Any agent that reads Agent Skills** (a folder with a `SKILL.md`): copy the folders. ## What an agent should read first 1. [llms.txt](https://diffusr.ai/docs/llms.txt), then the pages it links (each is plain Markdown). 2. The OpenAPI spec: https://app.diffusr.ai/api/v2/openapi.json 3. [Quickstart](https://diffusr.ai/docs/quickstart) and [Errors](https://diffusr.ai/docs/errors). Source: https://diffusr.ai/docs/mcp-and-skills --- # llms.txt > Every page here is also plain Markdown. Start from llms.txt. | What | Where | | --- | --- | | Index of every page, one line each | https://diffusr.ai/docs/llms.txt | | Every page in one file | https://diffusr.ai/docs/llms-full.txt | | One page as Markdown | add `.md` to its address, for example https://diffusr.ai/docs/quickstart.md (the overview is https://diffusr.ai/docs/index.md) | | OpenAPI 3.1 spec of the API | https://app.diffusr.ai/api/v2/openapi.json | | MCP server | https://app.diffusr.ai/api/mcp | Each page's HTML has `` pointing at its Markdown twin, and a "View as Markdown" link at the top. The code examples in these docs are generated from the same code as the Code button in the app, and the prices from the live model list, so they match the API. The pages are rebuilt when the API changes. Source: https://diffusr.ai/docs/llms-txt --- # Reference > One page per route of the REST API, built from the same OpenAPI spec the app serves. Base address: `https://app.diffusr.ai/api/v2`. Send `Authorization: Bearer ` on every call except `GET /models`. Raw spec (OpenAPI 3.1): https://app.diffusr.ai/api/v2/openapi.json | Route | What it does | | --- | --- | | [`POST /api/v2/make_video`](https://diffusr.ai/docs/reference/make_video.md) | Make a video from a prompt | | [`POST /api/v2/make_image`](https://diffusr.ai/docs/reference/make_image.md) | Make an image from a prompt | | [`POST /api/v2/edit_image`](https://diffusr.ai/docs/reference/edit_image.md) | Edit a picture with a prompt | | [`POST /api/v2/recast`](https://diffusr.ai/docs/reference/recast.md) | Recast a video clip | | [`POST /api/v2/creator_video`](https://diffusr.ai/docs/reference/creator_video.md) | Make a video of a creator saying a script | | [`POST /api/v2/make_music`](https://diffusr.ai/docs/reference/make_music.md) | Make a song from a prompt | | [`POST /api/v2/run_template`](https://diffusr.ai/docs/reference/run_template.md) | Run a template | | [`POST /api/v2/uploads`](https://diffusr.ai/docs/reference/upload_asset.md) | Upload a file to use as a reference | | [`GET /api/v2/results`](https://diffusr.ai/docs/reference/list_results.md) | List your recent jobs | | [`GET /api/v2/results/{id}`](https://diffusr.ai/docs/reference/get_result.md) | Get one job | | [`POST /api/v2/results/{id}/cancel`](https://diffusr.ai/docs/reference/cancel_result.md) | Stop a job that has not finished | | [`GET /api/v2/models`](https://diffusr.ai/docs/reference/list_models.md) | List models and their prices | | [`GET /api/v2/creators`](https://diffusr.ai/docs/reference/list_creators.md) | List creators for creator_video | | [`GET /api/v2/templates`](https://diffusr.ai/docs/reference/list_templates.md) | List the templates run_template can run | | [`GET /api/v2/callbacks`](https://diffusr.ai/docs/reference/list_callbacks.md) | List callback deliveries | | [`POST /api/v2/callbacks/{event_id}/retry`](https://diffusr.ai/docs/reference/retry_callback.md) | Retry a failed callback by hand | | [`GET /api/v2/callbacks/secret`](https://diffusr.ai/docs/reference/get_callback_secret.md) | Get your callback signing secret | | [`POST /api/v2/callbacks/secret/rotate`](https://diffusr.ai/docs/reference/rotate_callback_secret.md) | Make a new callback signing secret | | [`GET /api/v2/ads/accounts`](https://diffusr.ai/docs/reference/ads_list_accounts.md) | List connected ad accounts | | [`GET /api/v2/ads/creatives`](https://diffusr.ai/docs/reference/ads_list_creatives.md) | List creatives you can advertise | | [`GET /api/v2/ads/campaigns`](https://diffusr.ai/docs/reference/ads_list_campaigns.md) | List campaigns with stats | | [`POST /api/v2/ads/launches`](https://diffusr.ai/docs/reference/ads_draft_launch.md) | Draft an ad launch | | [`GET /api/v2/ads/launches/{id}`](https://diffusr.ai/docs/reference/ads_get_launch.md) | Get one ad launch | Source: https://diffusr.ai/docs/reference --- # make_video > Make a video from a prompt `POST /api/v2/make_video` Starts a video job. Send `dry_run: true` first to get the price without spending. Video takes 1-10 minutes: poll GET /results/{id} or pass `callback_url`. Needs the `generate` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `generate` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Headers | Name | Type | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | string | no | 1-255 visible ASCII characters you make up for this request (a UUID is fine). Send the same key to retry safely: you get the first job back instead of paying twice. Reusing a key for a different request answers 409 IDEMPOTENCY_CONFLICT. Wins over `idempotency_key` in the body. | ## Request body (JSON) | Name | Type | Required | Description | | --- | --- | --- | --- | | `prompt` | string | yes | The shot: subject, action, camera, setting, sound. | | `image_urls` | string[] | no | Optional reference pictures (a person, a product, a room, a first frame). Say in the prompt what each one is, by its place in this list — on Seedance: "Image 1 is Mia, the speaker". Public URLs — upload_asset gives you one for a local file. | | `audio_urls` | string[] | no | Optional voice or sound clips (models with takes_audio in list_models — the Seedance family). E.g. 5–10 s of the speaker's voice cut from an earlier clip, so the next clip keeps that voice. Name each in the prompt: "Audio 1 is Mia's voice". Public URLs. | | `duration_sec` | integer | no | Length in seconds. Leave out for the model's default. | | `aspect_ratio` | string | no | e.g. "9:16", "16:9", "1:1". Leave out for the model's default. | | `resolution` | string | no | e.g. "720p". Leave out for the model's default. | | `model` | string | no | Leave out for the recommended model. Ids come from list_models. | | `dry_run` | boolean | no | true = only check the request and return the price in credits. Nothing is made, nothing is spent. | | `idempotency_key` | string | no | A unique string you make up for this request. Retry with the same key and you get the first result back instead of paying again. | | `callback_url` | string | no | Optional. A public https:// link. When the job finishes (done, failed or canceled) we POST the finished result here, signed. See the docs for checking the signature. Polling get_result still works. | ## Example **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_video \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "A woman turns to camera on a sunny street, smiles", "model": "seedance-2-0-fast", "resolution": "720p", "duration_sec": 5, "aspect_ratio": "9:16" }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeVideo({ prompt: "A woman turns to camera on a sunny street, smiles", model: "seedance-2-0-fast", resolution: "720p", duration_sec: 5, aspect_ratio: "9:16", }); const result = await d.waitFor(job.id); console.log(result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_video( prompt="A woman turns to camera on a sunny street, smiles", model="seedance-2-0-fast", resolution="720p", duration_sec=5, aspect_ratio="9:16", ) result = d.wait_for(job["id"]) print(result["output"]["url"]) ``` Same call as a dry run (the price, nothing made, nothing spent): **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_video \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "A woman turns to camera on a sunny street, smiles", "model": "seedance-2-0-fast", "resolution": "720p", "duration_sec": 5, "aspect_ratio": "9:16", "dry_run": true }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeVideo({ prompt: "A woman turns to camera on a sunny street, smiles", model: "seedance-2-0-fast", resolution: "720p", duration_sec: 5, aspect_ratio: "9:16", dry_run: true, }); console.log(job); // the price, nothing was made ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_video( prompt="A woman turns to camera on a sunny street, smiles", model="seedance-2-0-fast", resolution="720p", duration_sec=5, aspect_ratio="9:16", dry_run=True, ) print(job) # the price, nothing was made ``` ## Response ### 200 ApiDryRun dry_run was true: the price, nothing made, nothing spent. | Name | Type | Required | Description | | --- | --- | --- | --- | | `dry_run` | true | yes | | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `credits_estimated` | number | yes | | | `price` | object | no | | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | | | `note` | string | yes | | ### 202 ApiStarted The job started. Poll GET /results/{id}. | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The job id. Poll GET /results/{id} with it, or wait for the callback. | | `status` | string | yes | "PENDING" on a new start; the run's current status when a retried Idempotency-Key replays the first job. | | `credits_estimated` | number | yes | The most this job can cost, in credits. | | `idempotent_replay` | true | no | true when this is the FIRST job again, answering a retried Idempotency-Key. | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `price` | object | no | The price, locked in at start. | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | Heads-ups that did not stop the call. | | `next` | string | yes | What to do next, in words. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 402: Not enough credits, or an API budget would be passed (INSUFFICIENT_CREDITS, BUDGET_EXCEEDED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 409: IDEMPOTENCY_CONFLICT or CALLBACK_NOT_RETRYABLE. - 413: The request or file is too large (TOO_LARGE, REFERENCE_TOO_LARGE). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Costs credits (1 credit = $0.01). Send `dry_run: true` first: it answers with the price and spends nothing. You are charged when the result is delivered and never more than the quoted price. Work that fails, is canceled before it runs, or is never delivered is not charged. A test key quotes the same price and charges nothing. It is priced at the direct rate (your prompt goes to the model as sent), the same rate as the model prices on the Prices page. --- # make_image > Make an image from a prompt `POST /api/v2/make_image` Starts an image job (about 10-30 seconds). Needs the `generate` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `generate` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Headers | Name | Type | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | string | no | 1-255 visible ASCII characters you make up for this request (a UUID is fine). Send the same key to retry safely: you get the first job back instead of paying twice. Reusing a key for a different request answers 409 IDEMPOTENCY_CONFLICT. Wins over `idempotency_key` in the body. | ## Request body (JSON) | Name | Type | Required | Description | | --- | --- | --- | --- | | `prompt` | string | yes | The picture: subject, setting, light, style. | | `image_urls` | string[] | no | Optional reference pictures. Public URLs. | | `aspect_ratio` | string | no | e.g. "9:16", "16:9", "1:1". Leave out for the model's default. | | `resolution` | string | no | e.g. "720p". Leave out for the model's default. | | `model` | string | no | Leave out for the recommended model. Ids come from list_models. | | `dry_run` | boolean | no | true = only check the request and return the price in credits. Nothing is made, nothing is spent. | | `idempotency_key` | string | no | A unique string you make up for this request. Retry with the same key and you get the first result back instead of paying again. | | `callback_url` | string | no | Optional. A public https:// link. When the job finishes (done, failed or canceled) we POST the finished result here, signed. See the docs for checking the signature. Polling get_result still works. | ## Example **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_image \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "A can of sparkling water on a wet street, soft morning light", "aspect_ratio": "1:1" }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeImage({ prompt: "A can of sparkling water on a wet street, soft morning light", aspect_ratio: "1:1", }); const result = await d.waitFor(job.id); console.log(result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_image( prompt="A can of sparkling water on a wet street, soft morning light", aspect_ratio="1:1", ) result = d.wait_for(job["id"]) print(result["output"]["url"]) ``` Same call as a dry run (the price, nothing made, nothing spent): **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_image \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "A can of sparkling water on a wet street, soft morning light", "aspect_ratio": "1:1", "dry_run": true }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeImage({ prompt: "A can of sparkling water on a wet street, soft morning light", aspect_ratio: "1:1", dry_run: true, }); console.log(job); // the price, nothing was made ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_image( prompt="A can of sparkling water on a wet street, soft morning light", aspect_ratio="1:1", dry_run=True, ) print(job) # the price, nothing was made ``` ## Response ### 200 ApiDryRun dry_run was true: the price, nothing made, nothing spent. | Name | Type | Required | Description | | --- | --- | --- | --- | | `dry_run` | true | yes | | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `credits_estimated` | number | yes | | | `price` | object | no | | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | | | `note` | string | yes | | ### 202 ApiStarted The job started. Poll GET /results/{id}. | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The job id. Poll GET /results/{id} with it, or wait for the callback. | | `status` | string | yes | "PENDING" on a new start; the run's current status when a retried Idempotency-Key replays the first job. | | `credits_estimated` | number | yes | The most this job can cost, in credits. | | `idempotent_replay` | true | no | true when this is the FIRST job again, answering a retried Idempotency-Key. | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `price` | object | no | The price, locked in at start. | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | Heads-ups that did not stop the call. | | `next` | string | yes | What to do next, in words. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 402: Not enough credits, or an API budget would be passed (INSUFFICIENT_CREDITS, BUDGET_EXCEEDED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 409: IDEMPOTENCY_CONFLICT or CALLBACK_NOT_RETRYABLE. - 413: The request or file is too large (TOO_LARGE, REFERENCE_TOO_LARGE). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Costs credits (1 credit = $0.01). Send `dry_run: true` first: it answers with the price and spends nothing. You are charged when the result is delivered and never more than the quoted price. Work that fails, is canceled before it runs, or is never delivered is not charged. A test key quotes the same price and charges nothing. It is priced at the direct rate (your prompt goes to the model as sent), the same rate as the model prices on the Prices page. --- # edit_image > Edit a picture with a prompt `POST /api/v2/edit_image` Changes the picture in image_urls[0] as the prompt says. Needs the `generate` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `generate` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Headers | Name | Type | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | string | no | 1-255 visible ASCII characters you make up for this request (a UUID is fine). Send the same key to retry safely: you get the first job back instead of paying twice. Reusing a key for a different request answers 409 IDEMPOTENCY_CONFLICT. Wins over `idempotency_key` in the body. | ## Request body (JSON) | Name | Type | Required | Description | | --- | --- | --- | --- | | `image_urls` | string[] | yes | The picture to edit first, then any extra references. Public URLs. | | `prompt` | string | yes | What changes, and what stays. e.g. "Make the can matte black. Keep the hand and the rain." | | `aspect_ratio` | string | no | e.g. "9:16", "16:9", "1:1". Leave out for the model's default. | | `resolution` | string | no | e.g. "720p". Leave out for the model's default. | | `model` | string | no | Leave out for the recommended model. Ids come from list_models. | | `dry_run` | boolean | no | true = only check the request and return the price in credits. Nothing is made, nothing is spent. | | `idempotency_key` | string | no | A unique string you make up for this request. Retry with the same key and you get the first result back instead of paying again. | | `callback_url` | string | no | Optional. A public https:// link. When the job finishes (done, failed or canceled) we POST the finished result here, signed. See the docs for checking the signature. Polling get_result still works. | ## Example **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/edit_image \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "Make the background a sunny beach, keep the can exactly as it is", "image_urls": [ "https://example.com/can.jpg" ] }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.editImage({ prompt: "Make the background a sunny beach, keep the can exactly as it is", image_urls: [ "https://example.com/can.jpg", ], }); const result = await d.waitFor(job.id); console.log(result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.edit_image( prompt="Make the background a sunny beach, keep the can exactly as it is", image_urls=[ "https://example.com/can.jpg", ], ) result = d.wait_for(job["id"]) print(result["output"]["url"]) ``` Same call as a dry run (the price, nothing made, nothing spent): **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/edit_image \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "Make the background a sunny beach, keep the can exactly as it is", "image_urls": [ "https://example.com/can.jpg" ], "dry_run": true }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.editImage({ prompt: "Make the background a sunny beach, keep the can exactly as it is", image_urls: [ "https://example.com/can.jpg", ], dry_run: true, }); console.log(job); // the price, nothing was made ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.edit_image( prompt="Make the background a sunny beach, keep the can exactly as it is", image_urls=[ "https://example.com/can.jpg", ], dry_run=True, ) print(job) # the price, nothing was made ``` ## Response ### 200 ApiDryRun dry_run was true: the price, nothing made, nothing spent. | Name | Type | Required | Description | | --- | --- | --- | --- | | `dry_run` | true | yes | | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `credits_estimated` | number | yes | | | `price` | object | no | | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | | | `note` | string | yes | | ### 202 ApiStarted The job started. Poll GET /results/{id}. | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The job id. Poll GET /results/{id} with it, or wait for the callback. | | `status` | string | yes | "PENDING" on a new start; the run's current status when a retried Idempotency-Key replays the first job. | | `credits_estimated` | number | yes | The most this job can cost, in credits. | | `idempotent_replay` | true | no | true when this is the FIRST job again, answering a retried Idempotency-Key. | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `price` | object | no | The price, locked in at start. | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | Heads-ups that did not stop the call. | | `next` | string | yes | What to do next, in words. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 402: Not enough credits, or an API budget would be passed (INSUFFICIENT_CREDITS, BUDGET_EXCEEDED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 409: IDEMPOTENCY_CONFLICT or CALLBACK_NOT_RETRYABLE. - 413: The request or file is too large (TOO_LARGE, REFERENCE_TOO_LARGE). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Costs credits (1 credit = $0.01). Send `dry_run: true` first: it answers with the price and spends nothing. You are charged when the result is delivered and never more than the quoted price. Work that fails, is canceled before it runs, or is never delivered is not charged. A test key quotes the same price and charges nothing. It is priced at the direct rate (your prompt goes to the model as sent), the same rate as the model prices on the Prices page. --- # recast > Recast a video clip `POST /api/v2/recast` Keeps a clip's motion, camera and timing and swaps who or what is in it. The price follows the clip's real length, read from the file. Needs the `generate` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `generate` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Headers | Name | Type | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | string | no | 1-255 visible ASCII characters you make up for this request (a UUID is fine). Send the same key to retry safely: you get the first job back instead of paying twice. Reusing a key for a different request answers 409 IDEMPOTENCY_CONFLICT. Wins over `idempotency_key` in the body. | ## Request body (JSON) | Name | Type | Required | Description | | --- | --- | --- | --- | | `video_url` | string | yes | The source clip, an MP4 or MOV link (upload_asset gives one). Its motion, camera and timing are kept. At most 10 s on the default model (30 s on seedance-2-5): a longer clip is refused before anything is spent, so trim it first. Its real length is read from the file and is what you are priced on. | | `image_urls` | string[] | no | Photos of who or what should appear in the clip. | | `mode` | "world" \| "swap" | no | "world" = same motion, new people, place and look from the photos. "swap" = replace the main subject with the first photo, keep the rest. Use this OR prompt. | | `prompt` | string | no | Your own instruction instead of a mode. Say the change, then what to keep. | | `duration_sec` | integer | no | Ignored. The output length follows your clip's real length, read from video_url (rounded, within the model's range). It is also what you are priced on. | | `aspect_ratio` | string | no | e.g. "9:16", "16:9", "1:1". Leave out for the model's default. | | `resolution` | string | no | e.g. "720p". Leave out for the model's default. | | `model` | string | no | Leave out for the recommended model. Ids come from list_models. | | `dry_run` | boolean | no | true = only check the request and return the price in credits. Nothing is made, nothing is spent. | | `idempotency_key` | string | no | A unique string you make up for this request. Retry with the same key and you get the first result back instead of paying again. | | `callback_url` | string | no | Optional. A public https:// link. When the job finishes (done, failed or canceled) we POST the finished result here, signed. See the docs for checking the signature. Polling get_result still works. | ## Example **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/recast \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "video_url": "https://example.com/clip.mp4", "mode": "swap", "image_urls": [ "https://example.com/new-person.jpg" ] }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.recast({ video_url: "https://example.com/clip.mp4", mode: "swap", image_urls: [ "https://example.com/new-person.jpg", ], }); const result = await d.waitFor(job.id); console.log(result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.recast( video_url="https://example.com/clip.mp4", mode="swap", image_urls=[ "https://example.com/new-person.jpg", ], ) result = d.wait_for(job["id"]) print(result["output"]["url"]) ``` Same call as a dry run (the price, nothing made, nothing spent): **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/recast \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "video_url": "https://example.com/clip.mp4", "mode": "swap", "image_urls": [ "https://example.com/new-person.jpg" ], "dry_run": true }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.recast({ video_url: "https://example.com/clip.mp4", mode: "swap", image_urls: [ "https://example.com/new-person.jpg", ], dry_run: true, }); console.log(job); // the price, nothing was made ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.recast( video_url="https://example.com/clip.mp4", mode="swap", image_urls=[ "https://example.com/new-person.jpg", ], dry_run=True, ) print(job) # the price, nothing was made ``` ## Response ### 200 ApiDryRun dry_run was true: the price, nothing made, nothing spent. | Name | Type | Required | Description | | --- | --- | --- | --- | | `dry_run` | true | yes | | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `credits_estimated` | number | yes | | | `price` | object | no | | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | | | `note` | string | yes | | ### 202 ApiStarted The job started. Poll GET /results/{id}. | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The job id. Poll GET /results/{id} with it, or wait for the callback. | | `status` | string | yes | "PENDING" on a new start; the run's current status when a retried Idempotency-Key replays the first job. | | `credits_estimated` | number | yes | The most this job can cost, in credits. | | `idempotent_replay` | true | no | true when this is the FIRST job again, answering a retried Idempotency-Key. | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `price` | object | no | The price, locked in at start. | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | Heads-ups that did not stop the call. | | `next` | string | yes | What to do next, in words. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 402: Not enough credits, or an API budget would be passed (INSUFFICIENT_CREDITS, BUDGET_EXCEEDED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 409: IDEMPOTENCY_CONFLICT or CALLBACK_NOT_RETRYABLE. - 413: The request or file is too large (TOO_LARGE, REFERENCE_TOO_LARGE). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Costs credits (1 credit = $0.01). Send `dry_run: true` first: it answers with the price and spends nothing. You are charged when the result is delivered and never more than the quoted price. Work that fails, is canceled before it runs, or is never delivered is not charged. A test key quotes the same price and charges nothing. It is priced at the outcome rate: a result we shape for you, so the rate is higher than the model prices on the Prices page. Use `dry_run: true` for the exact price. --- # creator_video > Make a video of a creator saying a script `POST /api/v2/creator_video` A talking video from a creator in GET /creators. Gated preview. Needs the `generate` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `generate` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Headers | Name | Type | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | string | no | 1-255 visible ASCII characters you make up for this request (a UUID is fine). Send the same key to retry safely: you get the first job back instead of paying twice. Reusing a key for a different request answers 409 IDEMPOTENCY_CONFLICT. Wins over `idempotency_key` in the body. | ## Request body (JSON) | Name | Type | Required | Description | | --- | --- | --- | --- | | `creator_id` | string | yes | From list_creators. | | `script` | string | yes | What the creator says to camera, word for word. Length of the video follows the script. | | `resolution` | "480p" \| "720p" | no | | | `dry_run` | boolean | no | true = only check the request and return the price in credits. Nothing is made, nothing is spent. | | `idempotency_key` | string | no | A unique string you make up for this request. Retry with the same key and you get the first result back instead of paying again. | | `callback_url` | string | no | Optional. A public https:// link. When the job finishes (done, failed or canceled) we POST the finished result here, signed. See the docs for checking the signature. Polling get_result still works. | ## Example **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/creator_video \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "creator_id": "CREATOR_ID_FROM_LIST_CREATORS", "script": "I tried this for a week and I am not going back." }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.creatorVideo({ creator_id: "CREATOR_ID_FROM_LIST_CREATORS", script: "I tried this for a week and I am not going back.", }); const result = await d.waitFor(job.id); console.log(result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.creator_video( creator_id="CREATOR_ID_FROM_LIST_CREATORS", script="I tried this for a week and I am not going back.", ) result = d.wait_for(job["id"]) print(result["output"]["url"]) ``` Same call as a dry run (the price, nothing made, nothing spent): **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/creator_video \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "creator_id": "CREATOR_ID_FROM_LIST_CREATORS", "script": "I tried this for a week and I am not going back.", "dry_run": true }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.creatorVideo({ creator_id: "CREATOR_ID_FROM_LIST_CREATORS", script: "I tried this for a week and I am not going back.", dry_run: true, }); console.log(job); // the price, nothing was made ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.creator_video( creator_id="CREATOR_ID_FROM_LIST_CREATORS", script="I tried this for a week and I am not going back.", dry_run=True, ) print(job) # the price, nothing was made ``` ## Response ### 200 ApiDryRun dry_run was true: the price, nothing made, nothing spent. | Name | Type | Required | Description | | --- | --- | --- | --- | | `dry_run` | true | yes | | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `credits_estimated` | number | yes | | | `price` | object | no | | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | | | `note` | string | yes | | ### 202 ApiStarted The job started. Poll GET /results/{id}. | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The job id. Poll GET /results/{id} with it, or wait for the callback. | | `status` | string | yes | "PENDING" on a new start; the run's current status when a retried Idempotency-Key replays the first job. | | `credits_estimated` | number | yes | The most this job can cost, in credits. | | `idempotent_replay` | true | no | true when this is the FIRST job again, answering a retried Idempotency-Key. | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `price` | object | no | The price, locked in at start. | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | Heads-ups that did not stop the call. | | `next` | string | yes | What to do next, in words. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 402: Not enough credits, or an API budget would be passed (INSUFFICIENT_CREDITS, BUDGET_EXCEEDED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 409: IDEMPOTENCY_CONFLICT or CALLBACK_NOT_RETRYABLE. - 413: The request or file is too large (TOO_LARGE, REFERENCE_TOO_LARGE). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Costs credits (1 credit = $0.01). Send `dry_run: true` first: it answers with the price and spends nothing. You are charged when the result is delivered and never more than the quoted price. Work that fails, is canceled before it runs, or is never delivered is not charged. A test key quotes the same price and charges nothing. It is priced at the outcome rate: a result we shape for you, so the rate is higher than the model prices on the Prices page. Use `dry_run: true` for the exact price. --- # make_music > Make a song from a prompt `POST /api/v2/make_music` One song, about a minute, on Lyria. Needs the `generate` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `generate` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Headers | Name | Type | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | string | no | 1-255 visible ASCII characters you make up for this request (a UUID is fine). Send the same key to retry safely: you get the first job back instead of paying twice. Reusing a key for a different request answers 409 IDEMPOTENCY_CONFLICT. Wins over `idempotency_key` in the body. | ## Request body (JSON) | Name | Type | Required | Description | | --- | --- | --- | --- | | `prompt` | string | yes | The song: genre, mood, instruments, tempo, and what it is for. Add lyrics with [Verse] / [Chorus] tags, or say "Instrumental only, no vocals." Lyria picks the length (about a minute); a length in words is only a hint. | | `dry_run` | boolean | no | true = only check the request and return the price in credits. Nothing is made, nothing is spent. | | `idempotency_key` | string | no | A unique string you make up for this request. Retry with the same key and you get the first result back instead of paying again. | | `callback_url` | string | no | Optional. A public https:// link. When the job finishes (done, failed or canceled) we POST the finished result here, signed. See the docs for checking the signature. Polling get_result still works. | ## Example **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_music \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "Upbeat lo-fi hip hop for a product ad, instrumental only, no vocals." }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeMusic({ prompt: "Upbeat lo-fi hip hop for a product ad, instrumental only, no vocals.", }); const result = await d.waitFor(job.id); console.log(result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_music( prompt="Upbeat lo-fi hip hop for a product ad, instrumental only, no vocals.", ) result = d.wait_for(job["id"]) print(result["output"]["url"]) ``` Same call as a dry run (the price, nothing made, nothing spent): **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/make_music \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "prompt": "Upbeat lo-fi hip hop for a product ad, instrumental only, no vocals.", "dry_run": true }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.makeMusic({ prompt: "Upbeat lo-fi hip hop for a product ad, instrumental only, no vocals.", dry_run: true, }); console.log(job); // the price, nothing was made ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.make_music( prompt="Upbeat lo-fi hip hop for a product ad, instrumental only, no vocals.", dry_run=True, ) print(job) # the price, nothing was made ``` ## Response ### 200 ApiDryRun dry_run was true: the price, nothing made, nothing spent. | Name | Type | Required | Description | | --- | --- | --- | --- | | `dry_run` | true | yes | | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `credits_estimated` | number | yes | | | `price` | object | no | | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | | | `note` | string | yes | | ### 202 ApiStarted The job started. Poll GET /results/{id}. | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The job id. Poll GET /results/{id} with it, or wait for the callback. | | `status` | string | yes | "PENDING" on a new start; the run's current status when a retried Idempotency-Key replays the first job. | | `credits_estimated` | number | yes | The most this job can cost, in credits. | | `idempotent_replay` | true | no | true when this is the FIRST job again, answering a retried Idempotency-Key. | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `price` | object | no | The price, locked in at start. | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | Heads-ups that did not stop the call. | | `next` | string | yes | What to do next, in words. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 402: Not enough credits, or an API budget would be passed (INSUFFICIENT_CREDITS, BUDGET_EXCEEDED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 409: IDEMPOTENCY_CONFLICT or CALLBACK_NOT_RETRYABLE. - 413: The request or file is too large (TOO_LARGE, REFERENCE_TOO_LARGE). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Costs credits (1 credit = $0.01). Send `dry_run: true` first: it answers with the price and spends nothing. You are charged when the result is delivered and never more than the quoted price. Work that fails, is canceled before it runs, or is never delivered is not charged. A test key quotes the same price and charges nothing. It is priced at the direct rate (your prompt goes to the model as sent), the same rate as the model prices on the Prices page. --- # run_template > Run a template `POST /api/v2/run_template` Runs one of the ready-made recipes from GET /templates (product ad, video template, talking head, podcast) as one job. Use `dry_run: true` for the price. Needs the `generate` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `generate` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Headers | Name | Type | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | string | no | 1-255 visible ASCII characters you make up for this request (a UUID is fine). Send the same key to retry safely: you get the first job back instead of paying twice. Reusing a key for a different request answers 409 IDEMPOTENCY_CONFLICT. Wins over `idempotency_key` in the body. | ## Request body (JSON) | Name | Type | Required | Description | | --- | --- | --- | --- | | `template` | string | yes | The template's id, from list_templates. | | `mode` | "remix" \| "recast" | no | Video templates only. "remix" (default) = make a new video like the template, from your pictures and prompt. "recast" = keep the template's moves and swap who is in it for your image_urls. | | `prompt` | string | no | Product ads: an optional change to the ad ("make it a winter sale"). Video templates: remix = your version of the template's prompt (leave out to use the template's own); recast = what to change. | | `pictures` | map of string | no | Video template remix: one picture per slot, keyed by the slot name from list_templates (e.g. {"him": "https://…"}). | | `image_urls` | string[] | no | Video template recast: photos of who should appear in the clip. | | `product_image_url` | string | no | Product ads: a photo of your product, uploaded with upload_asset. Talking head: optional, shows the product on camera. | | `script` | string | no | Talking head: what the person says, word for word. Podcast: one line per turn, written "Name: what they say", the names exactly as in people. Everything must fit one 30 s take. | | `place` | string | no | Talking head: where they are, in a few words, e.g. "a small Paris hotel room with a tall window". | | `people` | object[] | no | Talking head: one person. Podcast: two hosts, the first sits on the left. | | `people[].name` | string | yes | What the script calls them, e.g. "Mia". Plain first names work best. | | `people[].image_url` | string | yes | A clear photo of their face. A public link — upload_asset gives one for a local file. | | `people[].look` | string | no | Optional. Age, hair and outfit in a few words, e.g. "30s, short dark hair, green jacket". Leave out to keep what the photo shows. | | `setup` | "selfie" \| "propped" \| "friend" | no | Talking head on the plain template: how it is filmed — "selfie" (default), "propped" or "friend". | | `resolution` | "480p" \| "720p" | no | Leave out for the template's own default. 480p costs less. | | `dry_run` | boolean | no | true = only check the request and return the price in credits. Nothing is made, nothing is spent. | | `idempotency_key` | string | no | A unique string you make up for this request. Retry with the same key and you get the first result back instead of paying again. | | `callback_url` | string | no | Optional. A public https:// link. When the job finishes (done, failed or canceled) we POST the finished result here, signed. See the docs for checking the signature. Polling get_result still works. | ## Example **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/run_template \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "template": "tpl-talking-head", "script": "I tried this for a week and I am not going back.", "place": "a small Paris hotel room with a tall window", "people": [ { "name": "Mia", "image_url": "https://example.com/mia.jpg" } ] }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.runTemplate({ template: "tpl-talking-head", script: "I tried this for a week and I am not going back.", place: "a small Paris hotel room with a tall window", people: [ { name: "Mia", image_url: "https://example.com/mia.jpg", }, ], }); const result = await d.waitFor(job.id); console.log(result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.run_template( template="tpl-talking-head", script="I tried this for a week and I am not going back.", place="a small Paris hotel room with a tall window", people=[ { "name": "Mia", "image_url": "https://example.com/mia.jpg", }, ], ) result = d.wait_for(job["id"]) print(result["output"]["url"]) ``` Same call as a dry run (the price, nothing made, nothing spent): **cURL** ```bash curl -X POST https://app.diffusr.ai/api/v2/run_template \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "template": "tpl-talking-head", "script": "I tried this for a week and I am not going back.", "place": "a small Paris hotel room with a tall window", "people": [ { "name": "Mia", "image_url": "https://example.com/mia.jpg" } ], "dry_run": true }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const job = await d.runTemplate({ template: "tpl-talking-head", script: "I tried this for a week and I am not going back.", place: "a small Paris hotel room with a tall window", people: [ { name: "Mia", image_url: "https://example.com/mia.jpg", }, ], dry_run: true, }); console.log(job); // the price, nothing was made ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY job = d.run_template( template="tpl-talking-head", script="I tried this for a week and I am not going back.", place="a small Paris hotel room with a tall window", people=[ { "name": "Mia", "image_url": "https://example.com/mia.jpg", }, ], dry_run=True, ) print(job) # the price, nothing was made ``` ## Response ### 200 ApiDryRun dry_run was true: the price, nothing made, nothing spent. | Name | Type | Required | Description | | --- | --- | --- | --- | | `dry_run` | true | yes | | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `credits_estimated` | number | yes | | | `price` | object | no | | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | | | `note` | string | yes | | ### 202 ApiStarted The job started. Poll GET /results/{id}. | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The job id. Poll GET /results/{id} with it, or wait for the callback. | | `status` | string | yes | "PENDING" on a new start; the run's current status when a retried Idempotency-Key replays the first job. | | `credits_estimated` | number | yes | The most this job can cost, in credits. | | `idempotent_replay` | true | no | true when this is the FIRST job again, answering a retried Idempotency-Key. | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `price` | object | no | The price, locked in at start. | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `duration_sec` | number | no | | | `warnings` | string[] | no | Heads-ups that did not stop the call. | | `next` | string | yes | What to do next, in words. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 402: Not enough credits, or an API budget would be passed (INSUFFICIENT_CREDITS, BUDGET_EXCEEDED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 409: IDEMPOTENCY_CONFLICT or CALLBACK_NOT_RETRYABLE. - 413: The request or file is too large (TOO_LARGE, REFERENCE_TOO_LARGE). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Costs credits (1 credit = $0.01). Send `dry_run: true` first: it answers with the price and spends nothing. You are charged when the result is delivered and never more than the quoted price. Work that fails, is canceled before it runs, or is never delivered is not charged. A test key quotes the same price and charges nothing. It is priced at the outcome rate: a result we shape for you, so the rate is higher than the model prices on the Prices page. Use `dry_run: true` for the exact price. --- # upload_asset > Upload a file to use as a reference `POST /api/v2/uploads` Two ways in. Pass `source_url` (a public link) and we copy it. Or pass `content_type` + `size_bytes` for a local file: you get an `upload_url` to PUT the bytes to, then use `file_url` in a tool. Needs the `uploads` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `uploads` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Request body (JSON) | Name | Type | Required | Description | | --- | --- | --- | --- | | `source_url` | string | no | A public http(s) URL to copy in. Use this OR the three fields below. | | `content_type` | string | no | For a LOCAL file: its MIME type, e.g. "video/mp4", "image/jpeg". | | `size_bytes` | integer | no | For a LOCAL file: its exact size in bytes. | ## Example **cURL** ```bash curl -X POST "https://app.diffusr.ai/api/v2/uploads" \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -d '{ "content_type": "video/mp4", "size_bytes": 12345678 }' ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY // Copies a public link in. For a local file use d.uploadFile("./clip.mp4"). const up = await d.uploadFromUrl("https://example.com/clip.mp4"); console.log(up.file_url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY # Copies a public link in. For a local file use d.upload_file("./clip.mp4"). up = d.upload_from_url("https://example.com/clip.mp4") print(up["file_url"]) ``` ## Response ### 201 ApiUpload A file_url (URL copied in), or an upload_url to PUT the bytes to. ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # list_results > List your recent jobs `GET /api/v2/results` Needs the `results:read` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `results:read` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Query | Name | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | no | Default 20. | ## Example **cURL** ```bash curl "https://app.diffusr.ai/api/v2/results?limit=10" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const { results } = await d.listResults({ limit: 10 }); console.log(results.map((r) => r.status)); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY data = d.list_results(limit=10) print([r["status"] for r in data["results"]]) ``` ## Response ### 200 ApiResultList Your latest jobs, newest first. | Name | Type | Required | Description | | --- | --- | --- | --- | | `results` | object[] | yes | Newest first. | | `results[].id` | string | yes | | | `results[].status` | "PENDING" \| "RUNNING" \| "SUCCEEDED" \| "FAILED" \| "CANCELED" | yes | PENDING and RUNNING = keep waiting. SUCCEEDED, FAILED and CANCELED are final. | | `results[].model` | string | yes | | | `results[].credits_estimated` | number | no | | | `results[].created_at` | string | yes | | | `results[].finished_at` | string | no | | | `results[].output` | object | no | Present once SUCCEEDED. | | `results[].output.kind` | "IMAGE" \| "VIDEO" \| "AUDIO" | yes | | | `results[].output.url` | string | yes | Public link to the file. | | `results[].output.thumbnail_url` | string | no | | | `results[].output.width` | number | no | | | `results[].output.height` | number | no | | | `results[].output.duration_sec` | number | no | | | `results[].error` | object | no | Present when FAILED. | | `results[].error.code` | string | yes | | | `results[].error.message` | string | yes | | | `results[].request_id` | string | no | The X-Request-Id of the request that started the job. | | `results[].test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `results[].charge` | object | yes | | | `results[].charge.quoted_credits` | number \| null | yes | The most the call could charge (null for a run with no API quote). | | `results[].charge.charged_credits` | number | yes | What the call has charged so far. Work that was never delivered is not counted. A test run is always 0. | | `results[].charge.final` | boolean | yes | true once the run is over: the number can no longer change. | | `results[].price` | object | no | | | `results[].price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `results[].price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `results[].price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `results[].price.tariff_version` | string | yes | | | `results[].price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `results[].next` | string | no | Present while the job is not finished. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # get_result > Get one job `GET /api/v2/results/{id}` Status, output once it is done, and what it charged. Keep calling every 15-30 seconds until status is SUCCEEDED, FAILED or CANCELED. An id from the other key mode (test vs live) answers 404. Needs the `results:read` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `results:read` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Path | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The job id from a make call. | ## Example **cURL** ```bash curl "https://app.diffusr.ai/api/v2/results/run_8Hk2xxxxxxxx" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const result = await d.getResult("run_8Hk2xxxxxxxx"); console.log(result.status, result.output?.url); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY result = d.get_result("run_8Hk2xxxxxxxx") print(result["status"], result.get("output")) ``` ## Response ### 200 ApiResult The job. | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `status` | "PENDING" \| "RUNNING" \| "SUCCEEDED" \| "FAILED" \| "CANCELED" | yes | PENDING and RUNNING = keep waiting. SUCCEEDED, FAILED and CANCELED are final. | | `model` | string | yes | | | `credits_estimated` | number | no | | | `created_at` | string | yes | | | `finished_at` | string | no | | | `output` | object | no | Present once SUCCEEDED. | | `output.kind` | "IMAGE" \| "VIDEO" \| "AUDIO" | yes | | | `output.url` | string | yes | Public link to the file. | | `output.thumbnail_url` | string | no | | | `output.width` | number | no | | | `output.height` | number | no | | | `output.duration_sec` | number | no | | | `error` | object | no | Present when FAILED. | | `error.code` | string | yes | | | `error.message` | string | yes | | | `request_id` | string | no | The X-Request-Id of the request that started the job. | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `charge` | object | yes | | | `charge.quoted_credits` | number \| null | yes | The most the call could charge (null for a run with no API quote). | | `charge.charged_credits` | number | yes | What the call has charged so far. Work that was never delivered is not counted. A test run is always 0. | | `charge.final` | boolean | yes | true once the run is over: the number can no longer change. | | `price` | object | no | | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `next` | string | no | Present while the job is not finished. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 404: No such id, or it is not yours, or it belongs to the other key mode (NOT_FOUND). - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # cancel_result > Stop a job that has not finished `POST /api/v2/results/{id}/cancel` `charge` on the answer says what was charged. Work that was not delivered is not charged. Needs the `generate` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `generate` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Path | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The job id from a make call. | ## Example **cURL** ```bash curl -X POST "https://app.diffusr.ai/api/v2/results/run_8Hk2xxxxxxxx/cancel" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const result = await d.cancel("run_8Hk2xxxxxxxx"); console.log(result.status, result.charge); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY result = d.cancel("run_8Hk2xxxxxxxx") print(result["status"], result["charge"]) ``` ## Response ### 200 ApiResult The job after the cancel. A job that already finished comes back unchanged. | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `status` | "PENDING" \| "RUNNING" \| "SUCCEEDED" \| "FAILED" \| "CANCELED" | yes | PENDING and RUNNING = keep waiting. SUCCEEDED, FAILED and CANCELED are final. | | `model` | string | yes | | | `credits_estimated` | number | no | | | `created_at` | string | yes | | | `finished_at` | string | no | | | `output` | object | no | Present once SUCCEEDED. | | `output.kind` | "IMAGE" \| "VIDEO" \| "AUDIO" | yes | | | `output.url` | string | yes | Public link to the file. | | `output.thumbnail_url` | string | no | | | `output.width` | number | no | | | `output.height` | number | no | | | `output.duration_sec` | number | no | | | `error` | object | no | Present when FAILED. | | `error.code` | string | yes | | | `error.message` | string | yes | | | `request_id` | string | no | The X-Request-Id of the request that started the job. | | `test` | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. | | `charge` | object | yes | | | `charge.quoted_credits` | number \| null | yes | The most the call could charge (null for a run with no API quote). | | `charge.charged_credits` | number | yes | What the call has charged so far. Work that was never delivered is not counted. A test run is always 0. | | `charge.final` | boolean | yes | true once the run is over: the number can no longer change. | | `price` | object | no | | | `price.credits` | number | yes | The most this call costs, in whole credits (1 credit = $0.01). | | `price.usd` | string | yes | The same price in US dollars, e.g. "0.0336". | | `price.tariff` | "direct" \| "outcome" | yes | "direct" = your prompt goes to the model as sent; "outcome" = a result we shape (Recast, creator video). | | `price.tariff_version` | string | yes | | | `price.expires_at` | string | yes | The quoted price holds until then (ISO time). | | `next` | string | no | Present while the job is not finished. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 404: No such id, or it is not yours, or it belongs to the other key mode (NOT_FOUND). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. Work that was not delivered is not charged; the answer's `charge` says what was. --- # list_models > List models and their prices `GET /api/v2/models` Public: works without a key. With a key it counts toward your price-check limit. - Auth: Optional. Works without a key; a key counts toward your price-check limit. - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Query | Name | Type | Required | Description | | --- | --- | --- | --- | | `kind` | "video" \| "image" | no | | ## Example **cURL** ```bash curl "https://app.diffusr.ai/api/v2/models?kind=video" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const { models } = await d.listModels({ kind: "video" }); console.log(models.map((m) => m.id)); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY data = d.list_models(kind="video") print([m["id"] for m in data["models"]]) ``` ## Response ### 200 ApiModelList The models you can pass as `model`, with prices. | Name | Type | Required | Description | | --- | --- | --- | --- | | `models` | object[] | yes | | | `models[].id` | string | yes | Pass this as `model`. | | `models[].name` | string | yes | | | `models[].kind` | "video" \| "image" | yes | | | `models[].aspect_ratios` | string[] | yes | | | `models[].resolutions` | string[] | no | | | `models[].duration_sec` | object | no | | | `models[].duration_sec.min` | number | yes | | | `models[].duration_sec.max` | number | yes | | | `models[].duration_sec.default` | number | yes | | | `models[].max_reference_images` | number | yes | | | `models[].takes_video` | boolean | yes | | | `models[].takes_audio` | boolean | yes | Takes voice or sound clips in audio_urls. | | `models[].reference_labels` | object | no | | | `models[].reference_labels.image` | string | no | | | `models[].reference_labels.video` | string | no | | | `models[].reference_labels.audio` | string | no | | | `models[].cost` | object \| object \| object \| object | yes | The price as a simple number, at the API direct tariff. | | `models[].price` | object | yes | | | `models[].price.unit` | "second" \| "image" | yes | | | `models[].price.credits` | number | yes | Credits per second of video, or per image, at the settings in `at`. | | `models[].price.at` | object | yes | | | `models[].price.by_resolution` | map of number | no | The same price at each resolution, when the model has several. | | `models[].price.tariff` | "direct" | yes | | | `models[].price.tariff_version` | string | yes | | | `models[].about` | string | no | | | `models[].caveat` | string | no | | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # list_creators > List creators for creator_video `GET /api/v2/creators` Gated preview: accounts not on the list get the 404 a bad URL gets. Needs the `generate` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `generate` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Example **cURL** ```bash curl "https://app.diffusr.ai/api/v2/creators" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` ## Response ### 200 ApiCreatorList Creators you can use in creator_video. | Name | Type | Required | Description | | --- | --- | --- | --- | | `creators` | object[] | yes | | | `creators[].id` | string | yes | Pass this as creator_id to creator_video. | | `creators[].name` | string | yes | | | `creators[].blurb` | string | yes | | | `creators[].poster_url` | string | yes | | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 404: No such id, or it is not yours, or it belongs to the other key mode (NOT_FOUND). - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # list_templates > List the templates run_template can run `GET /api/v2/templates` Each template says what inputs it needs (name, type, required) and a rough price. Pass `dry_run: true` to run_template for the exact price. Needs the `generate` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `generate` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Example **cURL** ```bash curl "https://app.diffusr.ai/api/v2/templates" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const { templates } = await d.listTemplates(); console.log(templates.map((t) => t.id)); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY data = d.list_templates() print([t["id"] for t in data["templates"]]) ``` ## Response ### 200 ApiTemplateList Templates run_template can run, with the inputs each needs. | Name | Type | Required | Description | | --- | --- | --- | --- | | `templates` | object[] | yes | | | `templates[].id` | string | yes | Pass this as `template` to run_template. | | `templates[].name` | string | yes | | | `templates[].kind` | "product_ad" \| "video_template" \| "talking_head" \| "podcast" | yes | | | `templates[].about` | string | yes | | | `templates[].inputs` | object[] | yes | | | `templates[].inputs[].name` | string | yes | The run_template field this fills (a dotted name like `pictures.` is a key inside that field). | | `templates[].inputs[].type` | string | yes | What it takes, e.g. "text", "image_url", "video_url", "image_urls". | | `templates[].inputs[].required` | boolean | yes | | | `templates[].inputs[].about` | string | yes | | | `templates[].price_hint` | object | no | A rough price. The exact one comes from run_template with dry_run: true. | | `templates[].price_hint.credits` | number | yes | | | `templates[].price_hint.approximate` | true | yes | | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # list_callbacks > List callback deliveries `GET /api/v2/callbacks` Needs the `results:read` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `results:read` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Query | Name | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | no | Default 20. | | `cursor` | string | no | The next_cursor of the last page. | ## Example **cURL** ```bash curl "https://app.diffusr.ai/api/v2/callbacks?limit=20" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const log = await d.listCallbacks({ limit: 20 }); console.log(log); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY log = d.list_callbacks(limit=20) print(log) ``` ## Response ### 200 ApiCallbackDeliveryList Your callback delivery log, newest first. | Name | Type | Required | Description | | --- | --- | --- | --- | | `deliveries` | object[] | yes | Newest first. | | `deliveries[].event_id` | string | yes | | | `deliveries[].type` | "result.finished" | yes | | | `deliveries[].result_id` | string | yes | The job this event is about. | | `deliveries[].url` | string | yes | | | `deliveries[].status` | "PENDING" \| "DELIVERED" \| "FAILED" | yes | PENDING = will be tried (again); DELIVERED = your server answered 2xx; FAILED = we gave up. | | `deliveries[].attempts` | integer | yes | | | `deliveries[].last_status_code` | integer | no | | | `deliveries[].last_error` | string | no | | | `deliveries[].last_attempt_at` | string | no | | | `deliveries[].next_attempt_at` | string | no | | | `deliveries[].delivered_at` | string | no | | | `deliveries[].created_at` | string | yes | | | `deliveries[].test` | true | no | | | `next_cursor` | string | no | Pass as `cursor` for the next page. Absent on the last page. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # retry_callback > Retry a failed callback by hand `POST /api/v2/callbacks/{event_id}/retry` Only for a FAILED event; at most 3 manual retries per event (else CALLBACK_NOT_RETRYABLE). Needs the `results:read` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `results:read` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Path | Name | Type | Required | Description | | --- | --- | --- | --- | | `event_id` | string | yes | The callback event id ("evt_…"). | ## Example **cURL** ```bash curl -X POST "https://app.diffusr.ai/api/v2/callbacks/evt_3Qp9xxxxxxxx/retry" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY await d.retryCallback("evt_3Qp9xxxxxxxx"); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY d.retry_callback("evt_3Qp9xxxxxxxx") ``` ## Response ### 202 ApiCallbackDeliveryItem The event, queued for another try. | Name | Type | Required | Description | | --- | --- | --- | --- | | `event_id` | string | yes | | | `type` | "result.finished" | yes | | | `result_id` | string | yes | The job this event is about. | | `url` | string | yes | | | `status` | "PENDING" \| "DELIVERED" \| "FAILED" | yes | PENDING = will be tried (again); DELIVERED = your server answered 2xx; FAILED = we gave up. | | `attempts` | integer | yes | | | `last_status_code` | integer | no | | | `last_error` | string | no | | | `last_attempt_at` | string | no | | | `next_attempt_at` | string | no | | | `delivered_at` | string | no | | | `created_at` | string | yes | | | `test` | true | no | | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 404: No such id, or it is not yours, or it belongs to the other key mode (NOT_FOUND). - 409: IDEMPOTENCY_CONFLICT or CALLBACK_NOT_RETRYABLE. - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # get_callback_secret > Get your callback signing secret `GET /api/v2/callbacks/secret` Needs the `results:read` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `results:read` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Example **cURL** ```bash curl "https://app.diffusr.ai/api/v2/callbacks/secret" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const { secret } = await d.getCallbackSecret(); console.log(secret); // whsec_… ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY secret = d.get_callback_secret()["secret"] print(secret) # whsec_… ``` ## Response ### 200 ApiCallbackSecretView Your signing secret (made on the first ask). | Name | Type | Required | Description | | --- | --- | --- | --- | | `secret` | string | yes | Starts with "whsec_". Use it to check the webhook-signature header. | | `previous_valid_until` | string | no | After a rotation, the old secret still signs (a second signature) until this time. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # rotate_callback_secret > Make a new callback signing secret `POST /api/v2/callbacks/secret/rotate` Needs the `results:read` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `results:read` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Example **cURL** ```bash curl -X POST "https://app.diffusr.ai/api/v2/callbacks/secret/rotate" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` **TypeScript** ```ts import { Diffusr } from "diffusr"; const d = new Diffusr(); // reads DIFFUSR_KEY const { secret } = await d.rotateCallbackSecret(); ``` **Python** ```python from diffusr import Diffusr d = Diffusr() # reads DIFFUSR_KEY secret = d.rotate_callback_secret()["secret"] ``` ## Response ### 200 ApiCallbackSecretView The new secret. The old one keeps signing for 24 hours. | Name | Type | Required | Description | | --- | --- | --- | --- | | `secret` | string | yes | Starts with "whsec_". Use it to check the webhook-signature header. | | `previous_valid_until` | string | no | After a rotation, the old secret still signs (a second signature) until this time. | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # ads_list_accounts > List connected ad accounts `GET /api/v2/ads/accounts` Gated: accounts without ads get the 404 a bad URL gets. Needs the `ads:read` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `ads:read` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Example **cURL** ```bash curl "https://app.diffusr.ai/api/v2/ads/accounts" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` ## Response ### 200 ApiAdsAccountList Connected ad accounts and whether each is ready. | Name | Type | Required | Description | | --- | --- | --- | --- | | `accounts` | object[] | yes | | | `accounts[].id` | string | yes | | | `accounts[].platform` | string | yes | | | `accounts[].name` | string | yes | | | `accounts[].currency` | string | yes | | | `accounts[].ready` | boolean | yes | | | `accounts[].needs` | string \| null | yes | | | `accounts[].runs_as` | string \| null | yes | | | `settings_url` | string | yes | | | `next` | string | yes | | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 404: No such id, or it is not yours, or it belongs to the other key mode (NOT_FOUND). - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # ads_list_creatives > List creatives you can advertise `GET /api/v2/ads/creatives` Needs the `ads:read` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `ads:read` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Query | Name | Type | Required | Description | | --- | --- | --- | --- | | `kind` | "video" \| "image" | no | Leave out for both. TikTok takes videos only. | | `limit` | integer | no | Default 40, newest first. | ## Example **cURL** ```bash curl "https://app.diffusr.ai/api/v2/ads/creatives?limit=10" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` ## Response ### 200 ApiAdsCreativeList Videos and pictures you can put in an ad. | Name | Type | Required | Description | | --- | --- | --- | --- | | `creatives` | object[] | yes | | | `creatives[].id` | string | yes | | | `creatives[].kind` | "image" \| "video" | yes | | | `creatives[].url` | string | yes | | | `creatives[].thumbnail_url` | string \| null | yes | | | `creatives[].width` | number \| null | yes | | | `creatives[].height` | number \| null | yes | | | `creatives[].duration_sec` | number \| null | yes | | | `creatives[].created_at` | string | yes | | | `creatives[].fits` | map of any | yes | | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 404: No such id, or it is not yours, or it belongs to the other key mode (NOT_FOUND). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # ads_list_campaigns > List campaigns with stats `GET /api/v2/ads/campaigns` Needs the `ads:read` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `ads:read` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Query | Name | Type | Required | Description | | --- | --- | --- | --- | | `days` | integer | no | How many days of stats, counting today. Default 7. | ## Example **cURL** ```bash curl "https://app.diffusr.ai/api/v2/ads/campaigns?days=7" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` ## Response ### 200 ApiAdsCampaigns Campaigns with stats. ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 404: No such id, or it is not yours, or it belongs to the other key mode (NOT_FOUND). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent. --- # ads_draft_launch > Draft an ad launch `POST /api/v2/ads/launches` Makes a DRAFT only. Nothing launches or spends until a person opens review_url and presses Launch. Needs the `ads:write` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `ads:write` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Request body (JSON) | Name | Type | Required | Description | | --- | --- | --- | --- | | `ad_account_id` | string | yes | An id from ads_list_accounts. Its platform (Meta or TikTok) and currency come with it. | | `name` | string | no | The campaign's name. Leave out for " · ". | | `goal` | "traffic" \| "sales" | yes | "traffic" = visits to a site; "sales" = purchases, needs pixel_id. | | `pixel_id` | string | no | Sales only: the Meta pixel / TikTok pixel code to count purchases with. | | `daily_budget` | number | yes | What the whole launch may spend a day, in the account's currency as people say it: 20 = $20 a day. | | `start_at` | string | no | Leave out to start as soon as it's approved. | | `end_at` | string | no | Leave out to run until paused. | | `countries` | string[] | yes | Two-letter country codes, like ["US", "CA"]. | | `age_min` | integer | no | Default 18. | | `age_max` | integer | no | Default 65. | | `gender` | "all" \| "female" \| "male" | no | Default "all". | | `ads` | object[] | yes | One ad per video or picture, each with its own words. | | `ads[].creative_id` | string | yes | An id from ads_list_creatives. | | `ads[].text` | string | yes | The ad's words over the video. TikTok allows 100 characters, Meta far more (first ~125 show). | | `ads[].headline` | string | no | Meta only, optional: the bold line under the video. | | `ads[].cta` | "learn_more" \| "shop_now" \| "sign_up" \| "download" \| "contact_us" \| "book_now" | yes | The button. | | `ads[].url` | string | yes | Where the button goes. Must start with https://. | ## Example **cURL** ```bash curl -X POST "https://app.diffusr.ai/api/v2/ads/launches" \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -d '{ "ad_account_id": "ACCOUNT_ID", "goal": "traffic", "daily_budget": 20, "countries": [ "US" ], "ads": [ { "creative_id": "asset:CREATIVE_ID", "text": "Fresh and fizzy.", "cta": "shop_now", "url": "https://example.com" } ] }' ``` ## Response ### 201 ApiAdsLaunch A DRAFT with a review_url. | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `status` | string | yes | | | `step` | string \| null | yes | | | `error` | string \| null | yes | | | `platform` | string | yes | | | `account` | string | yes | | | `name` | string | yes | | | `daily_budget` | number | yes | | | `currency` | string | yes | | | `review_url` | string \| null | yes | | | `open_url` | string \| null | yes | | | `live_at` | string \| null | yes | | | `next` | string | yes | | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 404: No such id, or it is not yours, or it belongs to the other key mode (NOT_FOUND). - 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged. - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. Makes a draft only; nothing launches or spends until a person presses Launch in the app. --- # ads_get_launch > Get one ad launch `GET /api/v2/ads/launches/{id}` Needs the `ads:read` scope. - Auth: API key (Authorization: Bearer) - Key scope needed: `ads:read` - Raw spec: https://app.diffusr.ai/api/v2/openapi.json ## Path | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The job id from a make call. | ## Example **cURL** ```bash curl "https://app.diffusr.ai/api/v2/ads/launches/LAUNCH_ID" \ -H "Authorization: Bearer $DIFFUSR_KEY" ``` ## Response ### 200 ApiAdsLaunch The launch and where it stands. | Name | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `status` | string | yes | | | `step` | string \| null | yes | | | `error` | string \| null | yes | | | `platform` | string | yes | | | `account` | string | yes | | | `name` | string | yes | | | `daily_budget` | number | yes | | | `currency` | string | yes | | | `review_url` | string \| null | yes | | | `open_url` | string \| null | yes | | | `live_at` | string \| null | yes | | | `next` | string | yes | | ## Errors Every error body is `{ error, code, request_id, ... }`. See https://diffusr.ai/docs/errors. - 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED). - 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded). - 404: No such id, or it is not yours, or it belongs to the other key mode (NOT_FOUND). - 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry. - 500: Our fault (INTERNAL_ERROR). Quote request_id to support. - 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry. ## Charges Free. No credits are spent.