MakerNice

Quickstart

Generate a video with one API key and three requests: get a quote, create the generation, then poll until it's ready.

1. Create an API key

In the console, open Developers → API Keys and create a key. Send it as a Bearer token on every request. Generations are charged to your wallet; you can cap each key with a spend limit.

2. Get a quote

POSThttps://api.makernice.com/v1/generations/quote
curl
curl https://api.makernice.com/v1/generations/quote \
  -H "Authorization: Bearer $MAKERNICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"seedance-2.5","mode":"t2v","prompt":"A slow dolly shot through a greenhouse","duration":5,"resolution":"720p"}'

# → { "id": "qt_…", "amount": "500000", "expires_at": "…", "facts": { "quantity": 5, … } }

Amounts are in US dollars, in millionths of a dollar (micro-USD): 1,000,000 = $1.00. A quote locks the price for 12 minutes.

3. Create the generation

POSThttps://api.makernice.com/v1/generations
curl
curl https://api.makernice.com/v1/generations \
  -H "Authorization: Bearer $MAKERNICE_API_KEY" \
  -H "Idempotency-Key: 7c1e9a2b-order-1842" \
  -H "Content-Type: application/json" \
  -d '{"quote_id":"qt_…","input":{"model":"seedance-2.5","mode":"t2v","prompt":"A slow dolly shot through a greenhouse","duration":5,"resolution":"720p"}}'

# → 202 { "id": "gen_…", "status": "queued", "billing": "held", … }

The amount is held from your balance. If the generation fails or you cancel it while queued, the hold is released in full. Retrying with the same Idempotency-Key returns the same generation instead of charging twice.

4. Poll until it's ready

GEThttps://api.makernice.com/v1/generations/{id}
curl
curl https://api.makernice.com/v1/generations/gen_… \
  -H "Authorization: Bearer $MAKERNICE_API_KEY"

# status: queued → generating → moderating → succeeded | failed | canceled
# when succeeded: outputs[0].url → GET it with your key to receive a 302 to the file

5. Download the file

curl
curl -L -o video.mp4 https://api.makernice.com/v1/generations/gen_…/outputs/0 \
  -H "Authorization: Bearer $MAKERNICE_API_KEY"

Authentication

Every request needs an API key in the Authorization header, except GET /v1/models and GET /v1/pricing. Keys look like mn- followed by 48 letters and digits.

curl
curl https://api.makernice.com/v1/generations \
  -H "Authorization: Bearer $MAKERNICE_API_KEY"
  • The full key is shown only once, when you create it. We store only a fingerprint, so a lost key can't be recovered: create a new one and delete the old one.
  • A missing, malformed, unknown, disabled, expired or deleted key always gets the same 401 invalid_api_key response. Disabling or deleting a key takes effect on the next request.
  • Call the API from your server. It doesn't accept cross-origin browser requests (no CORS); never put a key in front-end code.
  • Every key in an account sees the same generations: any key can get, list and cancel all of the account's generations, including the ones created in the console.
  • In the console you can give a key an expiry date and a spend limit. A key's spend is the charged plus the currently held amount of the generations it created; a create request that would go over the limit gets 402 key_spend_limit_reached. Failed and canceled generations don't count.

Pricing & billing

  • Billing is in US dollars. All amounts are integer strings of micro-USD (millionths of a dollar): 1,000,000 = $1.00. Parse them as integers or decimals, not floating-point numbers.
  • Videos are priced per second (the quantity is the duration), images per request (one image per generation). Price = unit price × quantity.
  • A quote locks the price for 12 minutes (expires_at). The generation is charged the quoted amount, even if prices change in the meantime.
  • When you create a generation, the amount is held from your balance (billing: held). It's charged when the generation succeeds (charged) and released in full if it fails or is canceled (released). While a generation is checking, the amount stays held.

GET /v1/pricing lists the unit prices of the current price version. It needs no API key and may be cached for up to 60 seconds. official_unit_price, when present, is the official list price of the same combination for comparison only; you are always billed unit_price.

GEThttps://api.makernice.com/v1/pricing
Example response (illustrative values)
{
  "price_version": 3,
  "data": [
    { "model": "seedance-2.5", "mode": "t2v", "resolution": "720p", "unit": "second", "unit_price": "100000", "unit_price_usd": "0.10", "official_unit_price": "120000" }
  ]
}

Get a quote

Send the generation input; the response tells you exactly what the generation will cost. Each quote can be used for one generation.

POSThttps://api.makernice.com/v1/generations/quote
FieldTypeDescription
modelstringRequired. A model id from GET /v1/models.
modestringRequired. t2v, i2v, flf, r2v, t2i or i2i; each model lists the modes it supports.
promptstringRequired. 1–4,000 characters by default; some models allow fewer (see maxPromptChars in GET /v1/models).
durationintegerVideo only. Seconds, within the model's range; defaults to the model's default.
resolutionstringOne of the model's resolutions; defaults to defaultResolution. Not allowed when the model lists none.
aspect_ratiostringOne of the mode's aspectRatios.
image_urlstringFirst frame. Required for i2v and flf.
last_image_urlstringLast frame. Required for flf.
reference_imagesstring[]Required for r2v and i2i; at most the mode's maxReferences.
seedintegerOptional. 0–2,147,483,647. The same seed with the same settings gives closer results.
generate_audiobooleanOptional. Whether the video gets a soundtrack.
prompt_expansionstringOptional. off, balanced or quality: how much the model may rewrite your prompt.
negative_promptstringOptional. Up to 2,000 characters describing what to avoid.
watermarkbooleanOptional. Adds the provider's AI-generated watermark.

The last five fields are optional extras. Each model supports only some of them: GET /v1/models lists the ones available for every mode and resolution in prices[].options, with allowed values and defaults. A field no model route can apply returns 400 invalid_request; changing an optional field needs a new quote, like any other field.

Images are public https URLs (up to 2,048 characters). Fields that the mode doesn't use, and unknown fields, are rejected with 400 invalid_request; for input fields, error.details lists each problem as { path, message } (other invalid_request errors may have a different details shape or none). A combination without a price returns 422 price_unavailable.

Each image must be a publicly reachable https URL that serves a still JPEG, PNG or WebP image (animated WebP and GIF are not supported), at most 10 MB, directly at that URL: redirects are not followed. We fetch each image once and keep a copy; content moderation and the model both use that copy. If an image can't be used, the generation fails with invalid_request, error.input names the image, and the hold is released.

curl
curl https://api.makernice.com/v1/generations/quote \
  -H "Authorization: Bearer $MAKERNICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"seedance-2.5","mode":"t2v","prompt":"A slow dolly shot through a greenhouse","duration":5,"resolution":"720p"}'
Example response (illustrative values)
{
  "id": "qt_6f1c…",
  "amount": "500000",
  "price_version": 3,
  "expires_at": "2026-10-09T09:24:41.000Z",
  "facts": { "model": "seedance-2.5", "mode": "t2v", "resolution": "720p", "variant": "default", "unit": "second", "quantity": 5 }
}

Create a generation

Create the generation with the quote and the same input. The Idempotency-Key header is required (see Idempotency).

POSThttps://api.makernice.com/v1/generations
curl
curl https://api.makernice.com/v1/generations \
  -H "Authorization: Bearer $MAKERNICE_API_KEY" \
  -H "Idempotency-Key: 7c1e9a2b-order-1842" \
  -H "Content-Type: application/json" \
  -d '{"quote_id":"qt_6f1c…","input":{"model":"seedance-2.5","mode":"t2v","prompt":"A slow dolly shot through a greenhouse","duration":5,"resolution":"720p"}}'
  • Responds 202 with the generation (status: queued, billing: held). A retry with the same Idempotency-Key responds 200 with the original generation.
  • You may change the prompt, but changing anything that affects the price (model, mode, resolution, duration) returns 422 quote_mismatch.
  • A used quote returns 409 quote_already_used, an expired one 410 quote_expired, and one that isn't in your account 404 quote_not_found.
  • Not enough balance returns 402 insufficient_balance; going over the key's spend limit returns 402 key_spend_limit_reached.
  • Optional top-level callback_url (https, up to 2048 characters, no credentials in the URL, not a private or reserved address): when the generation finishes, we POST it there. It takes priority over your account's webhook address. See Webhooks.

Check status

Poll the generation until it reaches a final status. Polling every few seconds is fine; see the read limit in Rate limits.

GEThttps://api.makernice.com/v1/generations/{id}
StatusMeaning
queuedWaiting to start. Can still be canceled.
generatingThe model is working on it.
moderatingGeneration finished and the outputs are in a safety check; outputs stays empty until it completes. Keep polling.
succeededDone. The amount is charged and outputs are ready.
failedDidn't finish. The hold is released; error has a code and a message.
canceledCanceled while queued. The hold is released.
checkingWe're confirming the result. The amount stays held until it's resolved.

error.code of a failed generation is one of invalid_request (the model rejected the request; adjust the prompt or settings), content_rejected (rejected by content moderation or the model provider's safety policy; the hold is released), timeout, upstream_unavailable or generation_failed. For content_rejected, error.reason says what was rejected: prompt (the prompt), input_image (a reference image) or model (the model provider's safety policy). The same reason appears in the 422 response when a submitted prompt is blocked. When invalid_request is caused by a reference image that can't be used, error.input names it: image_url, last_image_url or reference_images[0] (zero-based). Replace that image and create a new generation.

Example response (illustrative values)
{
  "id": "gen_3b9e…",
  "type": "video",
  "model": "seedance-2.5",
  "mode": "t2v",
  "status": "succeeded",
  "prompt": "A slow dolly shot through a greenhouse",
  "params": { "duration": 5, "resolution": "720p", "aspectRatio": null },
  "amount": "500000",
  "billing": "charged",
  "price_version": 3,
  "outputs": [
    {
      "index": 0,
      "url": "/v1/generations/gen_3b9e…/outputs/0",
      "state": "stored",
      "contentType": "video/mp4",
      "expiresAt": "2026-11-08T09:14:03.000Z"
    }
  ],
  "error": null,
  "created_at": "2026-10-09T09:12:41.000Z",
  "finished_at": "2026-10-09T09:14:03.000Z"
}

90 days after a generation finishes, its request content is cleared under our data retention policy: prompt becomes an empty string. Billing details (model, params, amount, status and timestamps) are kept.

GET /v1/generations/{id}/events returns the generation's timeline. Codes: created, submitted, generating, delivered, failed_refunded, canceled_refunded, checking, requeued.

GEThttps://api.makernice.com/v1/generations/{id}/events
Example response (illustrative values)
{
  "data": [
    { "code": "created", "at": "2026-10-09T09:12:41.000Z" },
    { "code": "submitted", "at": "2026-10-09T09:12:43.000Z" },
    { "code": "generating", "at": "2026-10-09T09:12:50.000Z" },
    { "code": "delivered", "at": "2026-10-09T09:14:03.000Z" }
  ]
}

Download outputs

Each item in outputs has a url such as /v1/generations/gen_…/outputs/0, relative to https://api.makernice.com. Request it with your API key; the response is a 302 redirect to a short-lived link to the file. Request the endpoint again whenever you need the file instead of storing the redirect target.

GEThttps://api.makernice.com/v1/generations/{id}/outputs/{n}
curl
curl -L -o video.mp4 https://api.makernice.com/v1/generations/gen_…/outputs/0 \
  -H "Authorization: Bearer $MAKERNICE_API_KEY"
  • Add ?download=1 to have the file served as an attachment. This works once the file is stored; while it is pending or degraded the redirect goes to the original file and the parameter has no effect.
  • curl -L doesn't send your Authorization header to the redirect host, so your key stays with us.
  • Outputs exist only when the generation has succeeded; otherwise the endpoint returns 404 output_not_found.
  • state: pending (still being saved; the link already works), stored (saved until expiresAt), degraded (saving didn't complete; the link may stop working sooner — download it soon), expired (the saved file has expired; the endpoint returns 410 output_expired), blocked (the output didn't pass content moderation; url is an empty string — contact support for a manual review).

Cancel

Only queued generations can be canceled; the hold is released in full and the response is the canceled generation. Once a generation is generating it can't be stopped, and cancel returns 409 not_cancelable.

POSThttps://api.makernice.com/v1/generations/{id}/cancel
curl
curl -X POST https://api.makernice.com/v1/generations/gen_…/cancel \
  -H "Authorization: Bearer $MAKERNICE_API_KEY"

# → { "id": "gen_…", "status": "canceled", "billing": "released", … }

List generations

Lists the generations in your account, newest first — including the ones created in the console.

GEThttps://api.makernice.com/v1/generations
QueryDescription
limitPage size, 1–100. Default 20.
cursorThe next_cursor of the previous page.
statusOne or more statuses, comma-separated, e.g. queued,generating.
typevideo or image.
modelA model id.
qPart of a generation id, or text contained in the prompt (both case-insensitive).
curl
curl "https://api.makernice.com/v1/generations?status=queued,generating&limit=50" \
  -H "Authorization: Bearer $MAKERNICE_API_KEY"

# → { "data": [ …generations… ], "next_cursor": "gen_…" | null, "total": 12 }

total counts all generations matching the filters. When next_cursor is null, you've reached the last page.

Webhooks

Instead of polling, you can have us POST the generation to your server when it reaches a final status. We send to the callback_url given when creating the generation; without one, to your account's webhook address (set it in console settings). With neither, nothing is sent.

EventSent when
generation.succeededThe generation succeeded and was charged.
generation.failedThe generation failed; the held amount was released.
generation.canceledThe generation was canceled; the held amount was released.

Request

Example request (illustrative values)
POST /your/webhook/path HTTP/1.1
Content-Type: application/json
Makernice-Signature: t=1791364443,v1=5f0c2d…e81a
Makernice-Event-Id: evt_1b7f…
Example body (illustrative values)
{
  "id": "evt_1b7f…",
  "type": "generation.succeeded",
  "created_at": "2026-10-09T09:14:03.000Z",
  "data": {
    "id": "gen_3b9e…",
    "status": "succeeded",
    "billing": "charged",
    "outputs": [
      { "index": 0, "url": "https://api.makernice.com/v1/generations/gen_3b9e…/outputs/0", "state": "stored", … }
    ],
    …
  }
}
  • data is the same generation object as GET /v1/generations/{id}, except that outputs[].url is an absolute URL.
  • Makernice-Event-Id equals the body's id and stays the same across retries. Use it to ignore duplicates.

Verify the signature

Makernice-Signature is t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 with your signing secret over t + "." + raw body. Compute it over the raw bytes you received, compare in constant time, and reject requests whose t is more than 5 minutes from your clock. The signing secret is in console settings; callback_url deliveries are signed with the same secret. After you rotate it, deliveries not yet sent are signed with the new one.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the body exactly as received (Buffer). Don't re-serialize parsed JSON.
export function verifyWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(signatureHeader.split(",").map((p) => [p.slice(0, p.indexOf("=")), p.slice(p.indexOf("=") + 1)]));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}
Python
import hashlib
import hmac
import time


# raw_body: the body exactly as received (bytes). Don't re-serialize parsed JSON.
def verify_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance_seconds: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(",") if "=" in p)
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance_seconds:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Delivery and retries

  • HTTPS only. Redirects aren't followed. Each attempt times out after 10 seconds.
  • Any 2xx response counts as delivered. Respond quickly and do heavy work afterwards.
  • Otherwise we retry after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h — 7 attempts in total — and then mark the delivery failed. Recent deliveries and their last response are shown in console settings.
  • Each generation normally sends one event, and a delivery can arrive more than once (for example after a timeout on your side). If an output held back by content review is later released, we send the generation again with a new event id, so you may get two events for it; the latest one wins.

Models

GET /v1/models lists the models and the modes, resolutions and durations you can use. Only modes that have a price are listed, with their prices in modes[].prices. It needs no API key and may be cached for up to 60 seconds.

GEThttps://api.makernice.com/v1/models
  • resolutions / defaultResolution: allowed values for resolution. An empty list means the model has a fixed resolution and you leave resolution out. A listed resolution may have no price in some modes: check modes[].prices; quoting it returns 422 price_unavailable.
  • durations: { min, max, default } in seconds for videos, null for images.
  • modes[].needs: which images the mode requires (image → image_url, lastImage → last_image_url, references → reference_images, up to maxReferences).
  • modes[].aspectRatios: allowed aspect_ratio values. modes[].prices: unit price per resolution.
Example response (illustrative values)
{
  "data": [
    {
      "id": "seedance-2.5",
      "type": "video",
      "resolutions": ["480p", "720p", "1080p", "4k"],
      "defaultResolution": "720p",
      "durations": { "min": 4, "max": 30, "default": 5 },
      "modes": [
        {
          "mode": "t2v",
          "needs": { "image": false, "lastImage": false, "references": false },
          "maxReferences": null,
          "aspectRatios": ["adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"],
          "prices": [{ "resolution": "720p", "unit": "second", "unitPrice": "100000" }]
        }
      ]
    }
  ]
}

Errors

Errors are JSON: { "error": { "code", "message", "reason"?, "details"?, "request_id" } }. Branch on code; messages may change. Every response carries an x-request-id header (same value as request_id); include it when contacting support so we can find the request.

StatusCodeWhat to do
400invalid_requestThe body, query or a header is invalid. details lists each field and the problem; fix the request.
401invalid_api_keyThe key is missing, malformed, disabled, expired or deleted.
402insufficient_balanceTop up your wallet.
402key_spend_limit_reachedRaise the key's spend limit or use another key.
402daily_spend_limit_reachedYour account reached its daily spend limit; it resets at 00:00 UTC. Contact support to raise it.
403org_frozenYour account is frozen. Contact support.
404quote_not_foundThe quote doesn't exist in your account. Get a new quote.
404task_not_foundThe generation doesn't exist in your account. Check the id.
404output_not_foundThe generation hasn't succeeded or has no output with that index.
404not_foundUnknown endpoint. Check the method and path.
409quote_already_usedGet a new quote.
409not_cancelableOnly queued generations can be canceled.
410quote_expiredGet a new quote.
410output_expiredThe stored file has expired and can no longer be downloaded.
413payload_too_largeThe request body is over 64 KB. Shorten the prompt or send fewer image URLs.
422idempotency_conflictThis Idempotency-Key was used for a different request. Use a new key.
422quote_mismatchThe input changes the price (model, mode, resolution or duration). Quote this input again.
422price_unavailableThis combination has no price. Pick one listed by GET /v1/models.
422content_rejectedRejected by content moderation or the model provider's safety policy; error.reason says which (prompt, input_image or model). Change the prompt or images; you were not charged.
429rate_limitedWait for Retry-After seconds, then retry.
429concurrency_limit_reachedToo many generations in progress for your account. Wait for one to finish (see Retry-After), or contact support to raise the limit.
500internal_errorUnexpected server error. Retry with backoff; include request_id if you contact support.

For unexpected 5xx responses or network errors, retry with backoff. When retrying a create request, reuse the same Idempotency-Key so you're never charged twice.

Rate limits

Limits are per minute. Going over returns 429 rate_limited with a Retry-After header (seconds).

ScopeLimit
Per key — writes (quote, create, cancel)60 / min
Per key — reads (get, list, events, outputs)600 / min
Per account — writes across all keys120 / min
Per IP — models, pricing and requests with a rejected key120 / min

A key can also have at most 4 create requests in progress at the same time; more return 429 as well. Limits may be adjusted.

Each account also has two usage limits, by default 2 generations in progress at once and US$100 of spend per UTC day. Going over returns 429 concurrency_limit_reached (with Retry-After) or 402 daily_spend_limit_reached. Contact support to raise them.

Idempotency

  • POST /v1/generations requires an Idempotency-Key header: 1–200 printable ASCII characters, no spaces. Use a new value, such as a UUID, for each new generation.
  • Keys are unique within your account and shared by all of its API keys.
  • Repeating a request with the same key, quote and input returns the original generation with 200 and doesn't charge again.
  • Using the same key with a different quote_id, different input (including the prompt and image URLs) or a different callback_url returns 422 idempotency_conflict.
  • Quotes and cancels take no Idempotency-Key. Repeating a cancel that already succeeded returns 409 not_cancelable; check the status with GET.