Generate video from your own product with the models behind SeedAgent. Submit a job over plain HTTP/JSON, then collect the finished video by webhook or by polling. Usage is billed in SeedAgent credits per second of output, and failed renders are refunded automatically.
- Base URL:
https://seedagent.ai/api/v1 - Get a key: sign in at https://seedagent.ai/api-access/console and create one yourself — no approval, live immediately. See Authentication.
- Console: the same page shows your keys, balance, videos and API calls. See Console.
- Prices: per model and resolution at
https://seedagent.ai/api-access/pricing;
GET /v1/modelsreturns the same rates for your key. See Pricing. - Credits: top up by card at https://seedagent.ai/api-access/top-up. See Top-ups.
This reference is the contract for /api/v1. Fields and error codes are only
ever added, never repurposed or removed (the legacy webhook v1 signature
aside — see Verifying), and every change to the contract is
listed in the Revision history; a change that makes
validation stricter or alters documented behaviour or a field's value is
marked tightened or changed there. Prices, the model list and per-model
limits move day to day and are not listed in the history: they are published
by GET /v1/models and at https://seedagent.ai/api-access/pricing.
Base URL
https://seedagent.ai/api/v1
Endpoint paths in this reference, such as POST /v1/videos, include the /v1
version prefix and are relative to https://seedagent.ai/api: POST /v1/videos
is https://seedagent.ai/api/v1/videos. (Joining the Base URL above with
/v1/… would give /api/v1/v1/….)
Authentication
Every request carries a partner key:
Authorization: Bearer sk_live_a1b2c3d4e5f6...
Keys are self-serve. Sign in to https://seedagent.ai/api-access/console with your SeedAgent account (sign up first if you have none) and create a key — there is no approval step. A key is live as soon as it is created and spends that account's personal credit balance — what the SeedAgent app spends on personal canvases — never a workspace's shared credit pool. An account can hold up to 5 active keys, all sharing its balance — one per environment or per customer of yours, for example. Through the API each key sees only the jobs it created itself.
The plaintext key is shown once, when it is created or rotated, together
with the key's webhook signing secret (whsec_…, see
Webhooks). Store both straight away: we keep only a SHA-256 hash of
the key, so a lost key cannot be recovered — rotate it instead. Keys created
before per-key secrets existed can ask SeedAgent for theirs — no rotation
needed.
| Prefix | Meaning |
|---|---|
sk_live_ | Production key — every key created in the console is one. Requests charge the account's credit balance. |
sk_test_ | Reserved for a future sandbox. Today it behaves exactly like live — real charge, real render. Do not rely on it for free calls. |
Managing keys
Rotate and revoke your keys yourself in the console:
- Rotate replaces the key's secret. The key keeps its id, settings and
history, so jobs created before the rotation stay readable with the new
plaintext. The old plaintext stops working immediately — it then answers
401 invalid_api_key, notrevoked_api_key— and so does the old webhook signing secret: webhook deliveries from then on, redeliveries of earlier events included, are signed with the new one. Only a first delivery already under way can still carry the old signature, on its quick retries (for up to about 20 seconds — see Delivery); its later redeliveries use the new one. Deploy both new values together. Callbacks for jobs that finish between the rotation and your deploy are signed with the new secret; if your endpoint rejects them with a4xxthey are not retried (see Delivery). Answer a signature failure with a5xxso we retry, or reconcile withGET /v1/videosonce the new values are live. - Revoke shuts the key off at once: from then on it answers
401 revoked_api_key. Jobs it already submitted still finish (or are refunded) and their webhooks are still delivered. A revocation can't be undone from the console — to replace a revoked key, create a new one. If a key was revoked by mistake, SeedAgent can reactivate it on request. A new key starts fresh: through the API it can't read the revoked key's jobs — they answer404 not_foundand aren't in its list (the console's Videos shows them) — and the revoked key'sIdempotency-Keys don't apply to it, so re-sending one of its requests under the new key creates, and charges for, a new job. To shut off a leaked key but keep its jobs and idempotency, rotate it instead: the old plaintext stops working just as fast. - Creating keys is limited to 10 per hour per account and rotating them to 20 per hour. Revoking is never rate limited, so a leaked key can always be shut off.
SeedAgent can still lower a key's rate limit, restrict it to some models or revoke it — for example to stop abuse. Keys SeedAgent issued before keys were self-serve appear in their account's console and are managed the same way.
SeedAgent can also suspend an account's API access as a whole. While it is
suspended, a new submission from any of the account's keys is answered
403 account_suspended (nothing is charged), and creating or rotating keys in
the console, or starting a top-up, is refused with the
same code. Revoking keys still works. Contact contact@seedagent.ai about a
suspended account.
Rate limits
Two per-key buckets, each a fixed one-minute window:
| Bucket | Endpoints | Default |
|---|---|---|
| write | POST /v1/videos | 60 / minute (SeedAgent can set a different limit per key) |
| read | every GET (and HEAD) | 10 × the write limit |
Polling can therefore never starve your own submissions. Once the key is
accepted, every response — including a 429 — carries the state of the bucket
it was charged to:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window. |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Seconds until the window resets — whole seconds, rounded up, never 0. |
Exceeding a bucket returns 429 rate_limited with a Retry-After header (seconds
until the window resets, rounded up the same way).
Prefer webhooks to polling. If you poll, keep your total GETs
within the read bucket (its X-RateLimit-Limit; 600 a minute on a default
key). Polling each job with GET /v1/videos/{id} costs a few reads per job per
minute and suits up to roughly 100 jobs in flight. With more, poll
GET /v1/videos?limit=100 — with no status filter it returns your newest 100
jobs with their status, video_url and error — and page with
starting_after until you pass your oldest unfinished job. A job's status
changes about once a minute, so polling it more often than every ~30 s gains
little.
Headers
| Header | Direction | Notes |
|---|---|---|
Authorization | request | Bearer <key> — required. |
Content-Type | request | application/json on POST. |
Idempotency-Key | request | Optional on POST /v1/videos. See Idempotency. |
X-Request-Id | response | On every response from a documented endpoint and method, success or error — HEAD included (see Errors). Quote it to support, with the time of the call. |
X-RateLimit-* | response | See Rate limits. |
Retry-After | response | Seconds to wait, on 429 rate_limited, 409 idempotency_key_in_use and a 500 caused by a temporary authentication outage. |
Idempotent-Replayed | response | true when a POST /v1/videos was answered with an earlier job. See Idempotency. |
Errors
Every error from a documented endpoint shares one envelope:
{
"error": {
"type": "invalid_request_error",
"code": "unknown_model",
"message": "Unknown or unavailable model 'sa-video-x'. Call GET /v1/models for the current catalogue.",
"request_id": "0b7c6a4e-2f1d-4c1e-9a55-3e0d2f6b8c11"
}
}
Branch on code — it is stable and append-only. type groups codes into broad
classes; message is prose and may be reworded at any time. request_id equals
the X-Request-Id header.
| code | HTTP | type | Meaning |
|---|---|---|---|
missing_api_key | 401 | authentication_error | No Authorization: Bearer … header. |
invalid_api_key | 401 | authentication_error | Key not recognised — including the old plaintext of a rotated key. |
revoked_api_key | 401 | authentication_error | Key was revoked. |
rate_limited | 429 | rate_limit_error | Per-key rate limit exceeded. See Retry-After. |
invalid_request | 400 | invalid_request_error | Body, query or Idempotency-Key failed validation — including a body that is not valid JSON or not a JSON object. message names the field. Nothing was charged. |
unknown_model | 400 | invalid_request_error | Model id unknown, retired, not currently offered, or not enabled for this key. |
insufficient_credits | 402 | billing_error | Balance too low for the quoted cost. Nothing was charged — top up and retry. |
not_found | 404 | invalid_request_error | No such video for this key. |
internal_error | 500 | api_error | Our side, or the engine could not start the job. Any charge is refunded automatically — safe to retry (use the same Idempotency-Key), except as noted below. |
content_policy_violation | 400 | invalid_request_error | The engine's content moderation refused the prompt or inputs at submission. Any charge is refunded. Retrying the same request will fail again — change the prompt or inputs. |
idempotency_key_in_use | 409 | idempotency_error | A request with the same Idempotency-Key is still in flight. Retry after Retry-After. |
idempotency_key_reused | 422 | idempotency_error | The Idempotency-Key was already used with a different request body. |
account_suspended | 403 | permission_error | The account the key belongs to has had its API access suspended: no submission is accepted from any of its keys, and nothing was charged. Contact support. Returned by POST /v1/videos; creating or rotating a key in the console, or starting a top-up, is refused with the same code. See Managing keys. |
In rare cases a refund can't be recorded at once: the error's message then
says so, and the refund is retried automatically, landing within about 20
minutes. If it doesn't show in GET /v1/balance by then, quote the
request_id to support.
Rarer still, our billing service can fail without telling us whether the
charge went through, and message says so. Your balance may not settle it
either: it is a running total, not a list of per-request debits. Retry as usual with the same Idempotency-Key —
the retry is charged as a new request, even if the failed attempt's charge did
go through — and quote the failed attempt's request_id to support to recover
any charge it made: that charge is not refunded automatically.
An internal_error at submission can also mean the engine couldn't use one of
your inputs — message then says so, for example that an input image
couldn't be downloaded, and why (see GET /v1/models). The charge is refunded as
usual, but retrying the request unchanged fails again: fix the input first.
HEAD on a documented GET endpoint runs exactly like that GET —
authenticated, counted against the read bucket, with X-Request-Id and
X-RateLimit-* — and answers with the same status and headers, without a body.
OPTIONS on a documented path answers 204 with an Allow header.
Other requests outside the documented endpoints and methods, and failures at
the platform level, get a plain response with no JSON envelope, no
X-Request-Id and no X-RateLimit-* headers: an unknown path (404), an
unsupported method such as DELETE (405), a trailing slash (308,
redirecting to the path without it), a malformed URL (400), a request body
over 4.5 MB (413), or a gateway timeout or other 5xx. Handle an error body
that isn't JSON by its HTTP status, and treat a non-JSON 5xx on
POST /v1/videos like a client timeout: retry with the same Idempotency-Key
(see Idempotency).
GET /v1/models
The catalogue this key may generate with. Call it at integration time and periodically — models, durations, aspect ratios, prompt caps and prices move when a model is rolled out or retired, a model id is re-pointed at a new engine, or rates change, and this endpoint is how that movement is communicated. Do not hard-code the limits.
curl https://seedagent.ai/api/v1/models \
-H "Authorization: Bearer $SEEDAGENT_API_KEY"
{
"object": "list",
"data": [
{
"id": "sa-video-pro",
"name": "Seedance 2.0",
"description": "Flagship cinematic model...",
"type": "video",
"capabilities": {
"text_to_video": true,
"image_to_video": true,
"first_last_frame": true,
"reference_images": { "max": 9 },
"audio_output": true,
"seed": false,
"negative_prompt": false
},
"constraints": {
"duration": {
"min": 4,
"max": 15,
"presets": [4, 5, 10, 15],
"default": 4,
"presets_only": false,
"fixed": null
},
"aspect_ratios": ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "adaptive"],
"resolutions": ["480p", "720p", "1080p", "4k"],
"default_resolution": "720p",
"max_prompt_chars": 6000,
"min_image_size": { "width": 300, "height": 300 }
},
"pricing": {
"unit": "second",
"credits_per_second": 1,
"resolution_multipliers": { "480p": 1, "720p": 1, "1080p": 1.5, "4k": 3 },
"credits_per_second_by_resolution": { "480p": 1, "720p": 1, "1080p": 1.5, "4k": 3 },
"rounding": "ceil_per_job",
"minimum_charge": 1
}
}
]
}
(Values above are illustrative — read them from the live endpoint.)
- Model ids are stable public identifiers. The engine behind an id may
change; the id will not.
sa-video-proandsa-video-fastkeep their ids although they are now named Seedance 2.0 and Seedance 2.0 Fast. capabilities.seed/capabilities.negative_promptsay whether those fields reach the engine. A model that would ignore them rejects them instead.capabilities.reference_images.maxis at most 16, the mostreference_imagesa request may carry. A model whose engine documents no limit, or a higher one, is published at 16.nullmeans the model takes no reference images.constraints.duration.presets_only—true: only the values inpresetsare accepted;false: any whole number of seconds frommintomaxis, andpresetsare merely suggestions.constraints.duration.fixed—null, or a duration the engine always renders under some conditions:{ "seconds": 8, "resolutions": ["1080p", "4k"], "with_reference_images": true }means exactly 8 seconds whenever the resolution is1080por4k, or the request carriesreference_images.resolutionsuses the spelling ofconstraints.resolutions. See Duration.constraints.resolutionsisnullfor a model that takes no resolution;default_resolutionisnullthen too.min_image_sizeis enforced by the engine after submission: an input below it fails the job (refunded) rather than being rejected up front.nullmeans no minimum is published. The engines' other image limits — maximum dimensions, aspect ratio, file size, format — are not published and not checked before charging; an input that breaks one fails the job, which is refunded.- Some engines — today Veo 3.1 — download image inputs themselves during
submission. An image such an engine can't fetch — for example one not on a
public address, unreachable or answering with an HTTP error, over 20 MiB,
too slow to download, behind too many redirects, or with credentials in its
URL — is refused at submission with
internal_error(refunded, listed as afailedjob, no webhook) rather than failing the job later. max_prompt_charsis a character ceiling, not a guarantee that the engine reads the whole prompt: some engines count tokens rather than characters. Veo 3.1 reads at most 1,024 tokens — roughly 4,000 English characters, far fewer in Chinese or Japanese.
Which models appear
A model is listed while it is generally available on SeedAgent and priced at
every resolution it offers. Availability changes as models are rolled out or
retired. A key created in the console starts with every listed model; if
SeedAgent restricts a key to some of them, its allow-list narrows the list
further — the catalogue
returned for your key is authoritative. A model missing from
it answers unknown_model on POST /v1/videos.
The models the catalogue can contain today:
| id | name | inputs | notes |
|---|---|---|---|
sa-video-pro | Seedance 2.0 | text, image, first/last frame, reference images | Flagship; native audio. |
sa-video-fast | Seedance 2.0 Fast | text, image, first/last frame, reference images | Faster sibling of Seedance 2.0 at 480p / 720p; native audio. |
seedance-2.0-mini | Seedance 2.0 Mini | text, image, first/last frame, reference images | 480p and 720p only; native audio. |
seedance-2.5 | Seedance 2.5 | text, image, first/last frame, reference images | Shots up to 30 s; native audio. |
veo-3.1 | Veo 3.1 | text, image, first/last frame, reference images | Native audio, up to 4k. Discrete durations (presets_only); fixed 8 s at 1080p / 4k or with reference images. Reads at most 1,024 prompt tokens. Offered only at an API price. |
kling-3.0 | Kling 3.0 | text, image, first/last frame | Takes no resolution; one rate for the whole model. |
kling-3.0-omni | Kling 3.0 Omni | text, image, first/last frame, reference images | Takes no resolution; one rate for the whole model. |
wan-3.0 | Wan 3.0 | text, image, first/last frame, reference images | Native audio; up to 30 s. Resolutions spelled 480P / 720P / 1080P. With a frame input the output follows the frame's aspect ratio. |
wan-3.0-prime | Wan 3.0 Prime | text, image, first/last frame, reference images | Higher-fidelity Wan 3.0 tier, same inputs and limits. |
wan-2.7 | Wan 2.7 | text, image, first/last frame, reference images | Native audio; 720P / 1080P. With image_url or first/last frames the output follows the frame's aspect ratio. Offered only at an API price. |
happyhorse-1.1 | HappyHorse 1.1 | text, image | 720P / 1080P, up to 15 s. With image_url the output follows the image's aspect ratio. Offered only at an API price. |
"Inputs" maps to the request fields: text = prompt only, image = image_url,
first/last frame = first_frame_url + last_frame_url, reference images =
reference_images. Limits in the notes are indicative — read the live values
from the endpoint.
Pricing
- API prices are set per model and per resolution, and can differ from the SeedAgent app's. Where no API price is set, the model is billed at the app's current rate for it (app promotions included); for a model with separate text and image variants that is the higher of the two variants' app rates, so one published price holds for every request. Models noted "Offered only at an API price" above never follow an app rate: they are listed only while an API price is set at every resolution.
- The same list is published at https://seedagent.ai/api-access/pricing;
GET /v1/modelsremains the authoritative answer for your key. - One price per model and resolution, whether the request is text-only or carries images.
pricingis computed by the same function that chargesPOST /v1/videos, so it always matches the charge. Withrate = credits_per_second_by_resolution[resolution](orcredits_per_secondfor a model that takes no resolution), a job costsmax(minimum_charge, ceil(round(duration × rate, 6 decimals)))— whole credits, rounded up once per job (the 6-decimal rounding only removes floating-point noise such as7.000000000000001).minimum_chargeis1; every listed model has a positive rate at every resolution.credits_per_secondis the rate atdefault_resolution(the whole-model rate when the model takes no resolution).resolution_multipliersare each rate divided by it (to 6 decimal places).credits_per_second_by_resolutionandresolution_multipliersare keyed with exactly the spelling ofconstraints.resolutions(e.g."1080P"for Wan and HappyHorse,"1080p"for Seedance), and are bothnullfor a model that takes no resolution. Estimate a cost fromcredits_per_second_by_resolution, not fromcredits_per_second × resolution_multipliers: the multipliers are rounded to 6 decimals, so that product can land a hair above the real rate and round up to one credit more than the charge.- The price is fixed at submission. A rate change applies to jobs submitted
after it, never to one already accepted;
credits_chargedin the202is always the authoritative figure. - If prices cannot be read at the moment of the call,
GET /v1/modelsandPOST /v1/videosanswerinternal_error(nothing charged) rather than quote a price that was not published.
POST /v1/videos
Submit one generation. The job is handed to the engine before the response is
sent: the call usually returns within a few seconds, but when the engine is slow
it can take up to a few minutes. It returns 202 with the job's video object as
soon as the engine has accepted it; rendering itself is asynchronous. If the
engine hasn't taken the job after about 275 seconds, the call gives up and
answers internal_error (refunded, safe to retry).
Set your HTTP client timeout to at least 300 seconds and always send an
Idempotency-Key, so a retry after a timeout returns the job the first attempt
created instead of starting (and charging for) a second one.
A platform-level 5xx without a JSON body can still occur, rarely (see
Errors). Retry it with the same Idempotency-Key: for up to about
five minutes you may get 409 idempotency_key_in_use (keep honouring
Retry-After), then the queued job, if the failed attempt got as far as
creating one (if not, the retry is simply a new submission). That queued job
then turns failed (upstream_error, refunded, with a video.failed webhook)
within about 15 minutes; to resubmit, use a new key.
curl -X POST https://seedagent.ai/api/v1/videos \
-H "Authorization: Bearer $SEEDAGENT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-8812-shot-3" \
-d '{
"model": "sa-video-pro",
"prompt": "A lone figure on a rain-slicked rooftop at dusk.",
"duration": 5,
"aspect_ratio": "16:9",
"resolution": "1080p",
"webhook_url": "https://yourapp.com/hooks/seedagent",
"metadata": { "order_id": "8812" }
}'
Body
| Field | Type | Required | Notes |
|---|---|---|---|
model | string | yes | An id from GET /v1/models (at most 64 characters). |
prompt | string | yes | Up to the model's max_prompt_chars (never more than 6000). |
duration | integer | no | Seconds. Defaults to constraints.duration.default — or to constraints.duration.fixed.seconds where that rule applies. Must satisfy constraints.duration; see Duration. |
aspect_ratio | string | no | One of constraints.aspect_ratios (case-insensitive); an empty string is treated as omitted. Defaults to the first listed. Wan models and HappyHorse 1.1 follow the input frame's aspect ratio when given image_url (or, for Wan, first/last frames) — see Which models appear; the field is then echoed but has no effect. |
resolution | string | no | One of constraints.resolutions (case-insensitive); an empty string is treated as omitted. Defaults to constraints.default_resolution. Rejected, unless empty, for a model whose constraints.resolutions is null. |
image_url | string (url) | no | Seed frame for image-to-video. |
first_frame_url / last_frame_url | string (url) | no | First/last-frame interpolation. Supply both or neither. |
reference_images | string[] (url) | no | Character / style references, up to capabilities.reference_images.max (never more than 16). |
negative_prompt | string | no | Only for models with capabilities.negative_prompt: true. Up to 2000 characters; some engines use only a prefix and ignore the rest without an error — Wan 2.7 uses the first 500 characters. |
seed | integer | no | Only for models with capabilities.seed: true. 0–2147483647. |
webhook_url | string (url) | no | Called on completion or failure. Must be https on a publicly reachable hostname — not an IP address, no credentials, at most 2048 characters. We check only the URL's form when you submit: it is rejected unless it is https, without credentials, and its host is neither an IP address (IPv4 or IPv6) nor localhost or a name ending in .localhost, .local or .internal. Any other hostname is accepted without testing that it resolves or is reachable — one that doesn't resolve publicly is accepted, and its deliveries simply fail and are eventually given up, so reconcile with GET /v1/videos. See Webhooks. |
metadata | object (string→string) | no | Echoed back on every read and webhook, and filterable in GET /v1/videos. Up to 50 keys; keys 1–40 chars, values ≤ 500 chars. The key "__proto__" is reserved and rejected. A job sent without metadata reads metadata: null. |
Rules:
- One input mode per request: text only,
image_url,first_frame_url+last_frame_url, orreference_images. Combinations are rejected — the engines treat them as different task types and would otherwise silently drop part of what you sent. - Image fields must be
http(s)URLs of at most 2048 characters, publicly fetchable by the engine. A URL on a local or private host —localhost, a name ending in.localhost,.localor.internal, or a loopback, private, link-local, shared (CGNAT), multicast or reserved (0.0.0.0/8,240.0.0.0/4) IPv4 address — is rejected withinvalid_request, and so is any IPv6 address literal (use a hostname). - Photos of people on Seedance models. For
sa-video-pro,sa-video-fast,seedance-2.0-miniandseedance-2.5, every image input —image_url,first_frame_url/last_frame_urland each entry ofreference_images— is registered with the engine as a portrait asset before rendering, so photos of real people can pass the engine's review. An image that can't be registered is sent as-is and the job continues — the engine may then refuse it, as before. There is no extra charge and prices are unchanged. Registration can add a few seconds to the submission (more with many reference images), still within the about-275-second limit described underPOST /v1/videos. Other models receive your images unchanged. - Length limits —
max_prompt_chars,negative_prompt,metadatakeys and values,model, URLs — are counted in UTF-16 code units, like JavaScript'sString.length: most characters, including common Chinese, Japanese and Korean ones, count as 1; emoji and other characters outside the Basic Multilingual Plane count as 2. - String fields must be well-formed Unicode without U+0000: a string holding a
NUL character or an unpaired UTF-16 surrogate is rejected with
invalid_requestnaming the field. An unpaired surrogate usually comes from text cut in the middle of an emoji — when you truncate to a limit counted in UTF-16 code units, don't split a surrogate pair. - Unknown fields are rejected with
invalid_requestrather than ignored, so a typo surfaces immediately instead of silently changing the render. - An optional field sent as JSON
nullis treated exactly like leaving it out.modelandpromptcan't benull. - Some models have separate text-to-video and image-to-video variants behind
one id. You always send the one public id: a request with any image input
(
image_url, first/last frames orreference_images) is served by the image variant, a text-only request by the text variant, automatically and at the same price.capabilitiespublishes what either variant accepts;seed/negative_promptaretrueonly when every variant honours them, andmax_prompt_charsis the tightest limit of the two. - These checks, and those in Duration, run before anything is
charged: a request rejected with
invalid_requestcosts nothing.
Duration
duration is a whole number of seconds. A value that is not a whole JSON
number — 5.5 or "5", say — is rejected by body validation first; then the
model's rules apply, in this order:
- Fixed duration. If
constraints.duration.fixedapplies to the request — its resolution (including the default, when you omitresolution) is infixed.resolutions, orfixed.with_reference_imagesistrueand you sendreference_images— the model always rendersfixed.seconds. Omitdurationto get it, or send exactly that value; any other value is rejected with400 invalid_request. You are billed for the seconds rendered. - Presets only. If
constraints.duration.presets_onlyistrue, only the values inpresetsare accepted. - Range. The value must lie between
minandmax. - Per-resolution maximum. A model may cap the duration at a particular
resolution; a longer value there is rejected, and
messagestates the ceiling.
Each failure is 400 invalid_request with a message naming the allowed
values.
Response 202
The same video object GET /v1/videos/{id} returns — normally with status
processing, as the engine has already accepted the job:
{
"id": "b7c1e2f0-9a4d-4f77-9d0e-1c2b3a4d5e6f",
"object": "video",
"status": "processing",
"model": "sa-video-pro",
"video_url": null,
"duration": 5,
"aspect_ratio": "16:9",
"resolution": "1080p",
"credits_charged": 8,
"error": null,
"metadata": { "order_id": "8812" },
"created_at": "2026-09-25T10:14:02.412345+00:00",
"completed_at": null
}
created_at and completed_at are RFC 3339 timestamps in UTC with an explicit
+00:00 offset and 0–6 fractional digits, trailing zeros dropped — parse them
with an RFC 3339 / ISO 8601 parser, not a fixed-width pattern.
Nullable fields. resolution is null for a model whose
constraints.resolutions is null (today kling-3.0 and kling-3.0-omni);
metadata is null when none was sent; video_url and completed_at are
null unless the job is completed, and error unless it is failed. Every
other field always has a value.
Credits are charged at submission and the exact figure is credits_charged.
If the render fails, they are refunded automatically — no support ticket, no
reconciliation on your side.
If the engine refuses the job at submission, any charge is refunded and the
response is an error instead of a job: content_policy_violation (moderation —
change the request) or internal_error (safe to retry, unless message says
the engine couldn't use an input — see Errors). The refused attempt
is still recorded — it appears in GET /v1/videos as a failed job whose
charge was refunded, and it never triggers a webhook. A submission answered
internal_error because its job could not be queued may likewise be listed
as a failed, refunded job with no webhook.
Idempotency
Send an Idempotency-Key header (1–255 printable ASCII characters without
spaces, i.e. 0x21–0x7E — e.g. your own order or shot id plus an attempt
number) to make retries safe. A malformed key is rejected with
400 invalid_request.
The key names one attempt. A job that was accepted (202) stays bound to
its key for 24 hours, whatever happens to it later: if it fails, a retry with
the same key returns that failed job (202, Idempotent-Replayed: true — no
new charge, no new render, no new webhook). To render again, send a new key —
for example, append an attempt number (order-8812-shot-3-a2).
- A retry with the same key and the same body, within 24 hours, creates
nothing new: it returns the original job's current video object with
202and the headerIdempotent-Replayed: true— even if the model has since been withdrawn or re-priced (the original job keeps the price it was accepted at). - "The same body" is compared after JSON parsing: key order and whitespace
don't matter, any changed value does. Only an optional field sent as
nullis ignored in the comparison; any other value — an empty string,[],{}, or an explicit default such as"aspect_ratio": "16:9"— differs from leaving the field out, even where validation treats it the same. Send the same body on every retry. The same key with a different body →422 idempotency_key_reused. - The same key while the first request is still in flight →
409 idempotency_key_in_use; retry afterRetry-After. - A request that was answered with an error is not remembered — retrying it with the same key simply tries again.
- A request that got no answer — a client timeout, or a non-JSON
5xx— may already have queued a job. Retry it with the same key: you may get409for a few minutes, then that job (seePOST /v1/videos). - Two requests with the same key sent at the same moment normally resolve to
one job: the later one gets
409. While our in-flight check is briefly unavailable, the later one is instead charged, then refunded and answered with409or the earlier job (the debit can show on your balance for a moment); and if the balance covers only one of the two, the other may get402 insufficient_credits— nothing is charged for it, and a retry with the same key returns the accepted job. The backed-out request never shows up inGET /v1/videos. In rare cases, while the in-flight check is unavailable, two requests sent within milliseconds of each other can both be accepted as separate, separately charged jobs — so don't send the same key in parallel; retry only after a timeout or an error.
Idempotency keys are scoped to the API key. A replay counts against the write
rate limit like any other POST. Without an
Idempotency-Key, every POST is a new job and a new charge.
GET /v1/videos/{id}
Poll one generation. Scoped to the calling key: a video created by another key
— or an id that is not a video id at all — returns not_found, never 403.
{
"id": "b7c1e2f0-9a4d-4f77-9d0e-1c2b3a4d5e6f",
"object": "video",
"status": "completed",
"model": "sa-video-pro",
"video_url": "https://...",
"duration": 5,
"aspect_ratio": "16:9",
"resolution": "1080p",
"credits_charged": 8,
"error": null,
"metadata": { "order_id": "8812" },
"created_at": "2026-09-25T10:14:02.412345+00:00",
"completed_at": "2026-09-25T10:15:37.09+00:00"
}
Status values
| status | Meaning |
|---|---|
queued | Accepted, not started. |
processing | Rendering. |
completed | video_url and completed_at are populated. |
failed | error is populated. The credits are refunded automatically (see Billing) — credits_charged still shows the amount that was charged, and refunded. |
These four are the whole contract. Our pipeline has more internal stages; they
collapse into processing so new stages never break your state machine.
The error object
Present only when status is failed:
{
"code": "generation_failed",
"reason": "content_policy",
"message": "The request was rejected by content moderation. The credits charged for this request are refunded automatically."
}
| reason | Meaning |
|---|---|
content_policy | Moderation refused the prompt, an input, or the output. Resubmitting the same request will fail again. |
upstream_error | Anything else (engine error, timeout, storage, or an input the engine couldn't use). Worth retrying as a new request with a new Idempotency-Key (see Idempotency) — if the same inputs keep failing, change them. |
code is always generation_failed; branch on reason. More reasons may be
added — treat an unknown one like upstream_error.
If you registered a webhook_url, polling is only needed for reconciliation.
GET /v1/videos
List this key's generations, newest first.
# -g stops curl from treating [order_id] as a URL glob
curl -g "https://seedagent.ai/api/v1/videos?limit=20&status=failed&metadata[order_id]=8812" \
-H "Authorization: Bearer $SEEDAGENT_API_KEY"
| Query | Notes |
|---|---|
limit | 1–100, default 20. |
starting_after | A video id from the previous page — returns the videos created after it in this ordering (i.e. older). Must be one of this key's videos. |
status | queued, processing, completed or failed. |
metadata[<key>] | Exact match on a metadata value. Up to 5; all must match. |
{
"object": "list",
"data": [ { "id": "…", "object": "video", "status": "failed", "…": "…" } ],
"has_more": false
}
Page by passing the last id of data as starting_after while has_more is
true. Scoped to the calling key, like GET /v1/videos/{id}. An invalid query
value answers 400 invalid_request.
Query parameters not listed above are ignored, not rejected — a misspelled
filter returns the unfiltered list. Send metadata filters as
metadata[<key>]=<value> with a non-empty key. If a parameter is repeated,
limit, status and starting_after use its first value and
metadata[<key>] its last.
GET /v1/balance
The spendable credit balance of the account the key bills, and where to top it up. Every key under one account shares the balance.
{
"object": "balance",
"available_credits": 1240.5,
"top_up_url": "https://seedagent.ai/api-access/top-up"
}
available_creditsis a number of credits with at most two decimals.top_up_urlis the top-up page — link a low-balance alert to it. It is a web page for the account holder (they sign in there), not an API endpoint.
Webhooks
Every job with a webhook_url that turns terminal — completed or failed —
after its 202 gets a callback: we POST to that URL. A submission the engine
refused — answered with an error instead of a 202 — never does, although it
is listed as a failed job (see Response 202).
{
"id": "evt_4f0c2a9d1b7e4c3a8f5d6e7b9a0c1d2e",
"type": "video.completed",
"created": 1766918400,
"data": { "...": "the same object GET /v1/videos/{id} returns" }
}
type is video.completed or video.failed. created is the Unix time the
event was first prepared for delivery — not necessarily the time of the first
attempt — and, like id, it never changes across redeliveries.
Headers
| Header | Notes |
|---|---|
Content-Type | application/json |
X-Seedagent-Signature | t=<unix>,v1=<hex hmac>,v2=<hex hmac> — read the pairs by name; ignore any you don't use. |
X-Seedagent-Event | video.completed / video.failed |
X-Seedagent-Delivery | Stable for an event across every redelivery — use it (or the body's id) to dedupe. |
Verifying
HMAC-SHA256 over ${t}.${raw body} (Stripe's scheme):
v2is keyed with the webhook signing secret (whsec_…) of the key that submitted the job, shown with the key when it is created and replaced when it is rotated. With several keys, verify each callback with the secret of the key you submitted that job with. Deliveries after a rotation, redeliveries included, are signed with the new secret — apart from the quick retries of a first delivery already under way (see Managing keys). Verifyv2. (Keys created beforev2existed: ask SeedAgent for the secret, or rotate the key in the console to be shown a new one.)v1is keyed with a secret shared by all partners. It is kept only for integrations written beforev2existed and will be removed — it proves the call came from SeedAgent, not that it was meant for you.
The HMAC key is the whole secret string, whsec_ prefix included, as UTF-8
bytes — don't strip the prefix or decode the rest. Verify with the code below
rather than Stripe's SDK: Stripe's libraries check only v1, here the shared
legacy secret, so they can't verify v2.
import { createHmac, timingSafeEqual } from "crypto";
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=") as [string, string]),
);
if (!parts.t || !parts.v2) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v2);
if (a.length !== b.length || !timingSafeEqual(a, b)) return false;
// Reject stale callbacks — the timestamp is inside the signed payload, so a
// captured delivery cannot be replayed with a fresh one.
return Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
}
Verify against the raw request body, before any JSON parsing. Every attempt
is signed with a fresh t, so redeliveries verify normally.
If verification fails, answer 5xx rather than 4xx: a 4xx ends delivery of
that event for good (see Delivery), and a signature failure is
usually a secret you haven't deployed yet.
Delivery
- Your endpoint must answer
2xxwithin 8 seconds. - First attempt when the job turns terminal, with two quick retries (~1 s, ~4 s).
- If those fail (network error, timeout,
429or5xx), we keep retrying — roughly 1 min, 5 min, 30 min, 2 h, 6 h and 12 h after each previous failure, about 21 hours in all — then give up. - The schedule is a minimum: under heavy load an attempt can come later than scheduled.
- A
3xxor a4xxother than429is treated as a permanent rejection and not retried. Redirects are never followed. - Whatever happens to the callback, the job itself is complete — recover it with
GET /v1/videos/{id}orGET /v1/videos, using the key that created it (for a revoked key's jobs, use the console).
Billing
- Charged in credits against the balance of the SeedAgent account the key is
attached to — the account's personal credit balance, the one the SeedAgent
app spends on personal canvases, never a workspace's shared credit pool. Read
it with
GET /v1/balance; add to it with a top-up. - The first time a key is created for an account on or after 2026-09-28 — in the console or by SeedAgent — the account is credited a one-time welcome bonus (currently 20 credits): once per account, never again for further, rotated or replacement keys. Accounts whose keys all predate 2026-09-28 receive it with the next key created for them.
- Priced per second of output at the model's API rate for the chosen
resolution — set for the API independently of the SeedAgent app, or following
the app's rate where no API price is set — and rounded up to whole credits
once per job:
max(1, ceil(round(duration × rate, 6 decimals))). See Pricing; the list is also published at https://seedagent.ai/api-access/pricing. - The price is fixed at submission. The exact figure is returned as
credits_chargedin the202and never changes afterwards — a later rate change does not touch it, and nothing is metered after the render. - Failed renders are refunded automatically, in full — normally at once; in rare cases a refund that can't be recorded immediately is retried automatically and lands within about 20 minutes (see Errors).
- A submission that would overdraw the balance is rejected up front with
insufficient_credits; nothing is charged and nothing is queued.
Top-ups
Add credits to the balance your keys spend at https://seedagent.ai/api-access/top-up:
- Sign in as the SeedAgent account the API key belongs to. Top-ups are open to accounts that own at least one active (not revoked) API key — if you have none yet, create one in the console first — and whose API access is not suspended (see Managing keys).
- Enter any whole-dollar amount between the minimum and maximum shown on the page (currently US$10 and US$10,000). The page shows the credits it buys and any bonus that applies.
- Pay by card through Stripe Checkout.
Credits land on the balance the API bills as soon as Stripe confirms the payment — usually within a minute. The page lists your most recent top-ups (currently the latest 20) and their status.
- Rate. Shown on the page; currently US$1 = 7 credits. Credits are sold
whole: a top-up buys
floor(dollars × rate)credits. - Top-up bonus — once per account. Your account's first top-up of US$200 or more (currently) earns 20 % extra credits, rounded down. For example, a top-up of US$500 buys 3,500 credits plus a 700-credit bonus. Smaller top-ups before it don't use the chance up; once the bonus has been paid, later top-ups get none. If two qualifying payments are made close together, the bonus goes to the one credited to your balance first — normally, but not always, the one paid first.
- Welcome bonus. Separate from top-ups: a one-time welcome bonus (currently 20 credits) comes with an API key — once per account, not per key. See Billing for when it is paid.
- The rate and bonus terms shown when you start a payment are the ones applied to it, even if they change before the payment completes. If they changed after the page loaded, Pay first shows the new figures instead of going straight to checkout.
- The rate, limits and bonuses are configurable and may change; the top-up page always shows the current terms.
- Refunds and chargebacks. If a top-up payment is refunded or disputed through Stripe or your card issuer, its credits are not removed from the balance automatically; SeedAgent support handles the adjustment.
- There is no API endpoint for top-ups: they are made on the page, by the
account holder.
GET /v1/balancereturns the page's address astop_up_url.
Console
https://seedagent.ai/api-access/console is where you work with the API through your SeedAgent sign-in rather than a key. It is scoped to the signed-in account: you only ever see that account's own keys, videos and calls.
- Keys — create, rotate and revoke them (see Managing keys). A key is listed by its prefix only; the full key is never shown again after it is created or rotated.
- Balance — the credits your keys spend, the same figure
GET /v1/balancereturns. - Last 30 days — videos submitted, completed and failed, and credits charged, refunded and net. The totals count up to 5,000 videos — the most recent ones, if there are more.
- Videos — every video your keys created, newest first, with its status,
model, charge, refund and a link to the finished video. They are the objects
GET /v1/videosreturns, for all of the account's keys at once (through the API, each key sees only its own). Refunded shows the refund a failed job is owed; in rare cases it reaches your balance up to about 20 minutes later (see Errors). - Calls — the call log:
POST /v1/videosrequests made with your keys (accepted, replayed, or rejected once the key was recognised) andGET /v1/modelscalls, each with its endpoint, HTTP status, model, credits charged and error code. The row of a submission the engine refused shows the id of its failed job (click to copy) — find it in Videos, where its charge and refund show. A rate-limited request is logged once per key, bucket and minute — the first429— whichever endpoint it hit. Not logged: polling reads (GET /v1/videos,GET /v1/videos/{id},GET /v1/balance) apart from that first429, and requests refused before a key is recognised (a missing, unknown or revoked key). - CSV export of the Videos or Calls table: the file holds the rows loaded on screen, with the table's filters applied; use Load more until the list ends to include older rows.
Not in this version
Stated plainly so nothing here is inferred to exist:
- No image, speech or music endpoints — video only.
- No official SDKs. The API is plain HTTP/JSON.
- No progress percentage, queue position or ETA on
GET /v1/videos/{id}. - No cancellation of a submitted job.
- No sandbox:
sk_test_keys charge and render like live ones. - No per-key spend cap and no concurrency cap; the account balance is the ceiling.
- No API endpoint for managing keys: they are created, rotated and revoked in the console. A revoked key can't be restored from the console; SeedAgent can reactivate it on request.
- No image upload endpoint: inputs are URLs the engine fetches.
- No per-key price list: every key is charged the same published per-second rates.
- No top-up endpoint and no automatic recharge: credits are bought on the top-up page.
- Signed, expiring asset URLs are not issued —
video_urlis a durable public URL on our storage. - No deletion of a video, or of its public
video_url.
Revision history
2026-09-30
Every change below is additive, or a fix that brings behaviour in line with this reference, except where marked tightened (stricter validation) or changed (a documented behaviour or a field's value differs). Endpoints, fields and error codes are unchanged.
- Changed: image inputs to Seedance models (
sa-video-pro,sa-video-fast,seedance-2.0-mini,seedance-2.5) —image_url,first_frame_url/last_frame_urlandreference_images— are registered with the engine as portrait assets before rendering, so photos of real people can pass the engine's review. An image that can't be registered is sent as-is (the engine may then refuse it, as before). No extra charge and no price change; submission can take longer — a few seconds, more with many reference images — still within the about-275-second limit ofPOST /v1/videos. Request and response fields and error codes are unchanged.
2026-09-29 (later the same day)
Every change below is additive, or a fix that brings behaviour in line with this reference, except where marked tightened (stricter validation) or changed (a documented behaviour or a field's value differs). Endpoints, fields and error codes are unchanged.
- Tightened: a string field containing U+0000 or an unpaired UTF-16
surrogate (half of a split emoji) is rejected with
400 invalid_requestnaming the field (before: the request was charged, then refunded withinternal_error, on every attempt). - Tightened: an image URL on a local or private host —
localhost, a name ending in.localhost,.localor.internal, or a loopback, private, link-local, shared (CGNAT), multicast or reserved IPv4 address — is rejected with400 invalid_request, since no engine can fetch it; IPv6 address literals are not accepted either (use a hostname). - Changed:
completed_atisnullunlessstatusiscompleted(before, it could appear early on aprocessingjob, or stay set on afailedone). - Changed: a failed job's
error.messagesays the credits "are refunded automatically" (before: "were refunded", even while the refund was still pending). - Changed: when an input image can't be downloaded at submission (today
Veo 3.1 downloads its images itself), the
internal_errormessagenames the reason and no longer says it is safe to retry unchanged. - Changed: if we can't verify the account's status at submission,
POST /v1/videosanswersinternal_error(nothing charged) instead of proceeding. - Changed: a submission answered
internal_errorbecause its job could not be queued is usually listed inGET /v1/videosand the console as afailed, refunded job with no webhook (before: only sometimes). - Changed: when billing can't confirm whether a charge went through,
messagenow says to retry (the retry is charged as a new request) and to quote therequest_idto recover any charge from the failed attempt. - Fixed: a
409or422answered to a concurrent duplicate that was charged and backed out says so when its own refund is still pending. - Fixed: a submission whose job could not be queued can no longer be refunded twice; a refund that fails at first is always found by the automatic retry, however many failed jobs came before it; background clean-up after a late failure finishes within the request's lifetime.
- Fixed: an unresponsive internal cache no longer stalls API requests.
- Top-up page: an amount such as
10.000is refused (before: read as US$10); if the rate or bonus terms changed since the page loaded, Pay shows the new figures instead of going straight to checkout. - Console: the key list loads, and keys can be revoked, even when the balance or the 30-day totals can't be read; CSV export is disabled while a table is reloading; key ownership checks no longer depend on the list; a key that was created but never shown to you is revoked.
- Clarified wording (no behaviour change): the Revision history marks contract
changes, while prices, the model list and per-model limits are published by
GET /v1/modelsand the pricing page; a rotated key's old plaintext answersinvalid_api_key, and a first delivery's quick retries can still carry the old signature;HEADandOPTIONS; quotingX-Request-Idwith the time of the call; wheninternal_erroris not safe to retry unchanged; estimating a cost fromcredits_per_second_by_resolution; engines that download image inputs at submission; the exactwebhook_urlhost forms rejected at submission; a queued job after a platform5xx; idempotency for a request that got no answer, what counts as "the same body", and the concurrency caveat;upstream_errorincludes an input the engine couldn't use; which jobs get a webhook, whatcreatedrecords, which key's secret signsv2, and the delivery schedule as a minimum; the console's call-log job ids, refund timing and 5,000-video totals; videos can't be deleted.
2026-09-29
Every change below is additive, or a fix that brings behaviour in line with this reference, except where marked tightened (stricter validation) or changed (a published value or response differs). Endpoints, fields and error codes are unchanged.
- Fixed: a retry with the same
Idempotency-Keywhile the first request is still being handed to the engine now answers409 idempotency_key_in_use, as documented, instead of202with the not-yet-started job (which, if the engine then refused it, failed without a webhook). - Changed (loosened): an optional body field sent as JSON
nullis treated as omitted (before:400 invalid_request). - Tightened: the
metadatakey"__proto__"is rejected (before: accepted and silently dropped). - Changed: a
durationof0, a negative value or one above the model's maximum is answered with the model's allowed values (still400 invalid_request); a body that is not valid JSON, or not a JSON object, gets amessagesaying so. - Changed:
POST /v1/videosgives up handing a job to the engine after about 275 seconds and answersinternal_error(refunded) instead of the request timing out at the platform. - Fixed:
X-RateLimit-ResetandRetry-Afterare rounded up and never report60in the last moments of a window. - Fixed: a refund that fails at first is retried automatically; a job that failed is always refunded.
- Fixed: webhook retries reach every account and follow the documented schedule
even when the first attempt was made by our background retry sweep;
createdstays the same across redeliveries. - Fixed: a job can no longer read
completedwithvideo_urlnull. - Fixed: an
Idempotency-Keyreplay answers with the key's first job. - Fixed: a
metadata[__proto__]filter onGET /v1/videosis applied (before: ignored). - Fixed: a cold instance no longer waits indefinitely on an unreachable internal cache (requests used to hang until a gateway timeout).
- Changed:
wan-2.7publishesmin_image_size240×240 (wasnull; the engine's limit itself is unchanged). - Top-up page: an account whose API access is suspended sees that up front
instead of a pay form; past top-ups stay listed after the account's last key
is revoked; amounts such as
50.00,$50or1,000are read as whole dollars, and one with cents such as49.50is refused instead of being read as4950. - Console: a key creation refused because the account already has 5 active keys, or a rotation refused because the key is unknown or revoked, no longer counts toward the hourly limits. A submission the engine refused now links to its failed job in the call log. The first rate-limited request of every minute is logged again, and the key creation and rotation limits can no longer get stuck.
- Fixed: if paying the welcome bonus fails, the console retries it for every account it is owed to (before: not for an account whose first key predates 2026-09-28).
- Clarified wording (no behaviour change): the path convention under
Base URL; the personal balance versus a workspace's pool;
where
X-Request-Idand the error envelope apply, and the non-JSON platform responses; the timestamp format; nullable fields; length limits in UTF-16 code units; an emptyaspect_ratio/resolution; a revoked key's jobs and idempotency; rotation and signature failures (answer5xx); verifying with the wholewhsec_string, not Stripe's SDK; polling versus the read budget; unknown list query parameters are ignored; anIdempotency-Keynames one attempt (use a new key after a failure); the concurrency caveat; the form-onlywebhook_urlcheck; the Veo 3.1 token limit, the Wan 2.7negative_promptprefix and unpublished image limits; the welcome bonus for accounts keyed before 2026-09-28; the top-up bonus goes to the payment credited first; the top-up page shows the latest 20; the console's CSV holds the loaded rows; the intro's wording on tightened / changed items. The entry first published as 2026-09-29 was re-dated 2026-09-28 (later the same day), the day it shipped, and its welcome-bonus bullet corrected.
2026-09-28 (later the same day)
Every change below is additive except where marked changed. /api/v1
itself — endpoints, fields and prices — is unchanged; one error code was added.
- New: self-serve API keys. Any signed-in SeedAgent account creates its own keys at https://seedagent.ai/api-access/console — up to 5 active keys per account, live immediately, billed to that account's balance. Requesting a key from SeedAgent is no longer needed. See Authentication.
- New: the console — keys, balance, 30-day totals, every video and the call log, with CSV export, scoped to the signed-in account.
- New: rotation and revocation are self-serve in the console (see Managing keys); before, SeedAgent did both on request. Rotation keeps the key's id and history, and the old secret stops working at once, as before. Keys SeedAgent issued earlier appear in their account's console too.
- The welcome bonus is paid the first time a key is created for an account on or after 2026-09-28, in the console or by SeedAgent — still once per account. Accounts whose keys all predate 2026-09-28 receive it with the next key created for them.
- Changed: the top-up bonus is now +20 % on a top-up of US$200 or more, once per account (before: +30 % on the account's very first top-up if it bought more than 200 credits). Accounts that already received the bonus don't get a second one; a top-up opened before the change is credited on the terms it showed.
- New error code
account_suspended(403,permission_error): the account's API access is suspended, so no submission is accepted from any of its keys; creating or rotating a key in the console and starting a top-up are refused with it too (see Managing keys). Additive — no existing code changed. - Key-management limits are now separate: 10 key creations and 20 rotations per hour per account, and revocation is never rate limited (before: 10 changes of any kind per hour).
- Clarified wording: a revocation can't be undone from the console, but SeedAgent can reactivate a key on request; what the console's call log does and doesn't record (see Console); and a refunded or disputed top-up payment doesn't remove its credits automatically — support handles it (see Top-ups).
2026-09-28
Every change below is additive except where marked tightened (stricter validation) or changed (a published value differs). Model ids, request fields and error codes are unchanged.
- More models: besides
sa-video-pro/sa-video-fast, the catalogue can now listseedance-2.0-mini,seedance-2.5,veo-3.1,kling-3.0,kling-3.0-omni,wan-3.0,wan-3.0-prime,wan-2.7andhappyhorse-1.1(see Which models appear). A model with separate text and image variants is served by the matching variant automatically. - Tightened: listing rules. A model —
sa-video-*included — is listed only while it is generally available on SeedAgent (enabled, out of limited beta and offered in the app) and priced at every resolution; otherwise it is absent andPOST /v1/videosanswersunknown_model, as for a retired model. Before, only the enabled switch applied.sa-video-*meet every rule today. - Independent API pricing: each model and resolution can carry its own API price; where none is set the model follows the SeedAgent app's rate (as every model did before). The list is published at https://seedagent.ai/api-access/pricing. See Pricing.
- New catalogue fields:
constraints.duration.presets_only,constraints.duration.fixed. credits_per_second_by_resolution/resolution_multipliersare keyed with exactly the spelling ofconstraints.resolutions(e.g.1080P). Unchanged forsa-video-*, whose resolutions are lower-case.- Tightened: a model with discrete durations (
presets_only: true) rejects a non-presetdurationwithinvalid_request(before, it was accepted, then snapped to another length or rejected by the engine). Adurationthat conflicts withconstraints.duration.fixed, or exceeds a model's maximum at the requested resolution, is rejected the same way.sa-video-*are unaffected today (presets_only: false, nofixed). - Changed: the 6-decimal rounding in the pricing formula now applies to every job (it used to apply only to some rates). It removes floating-point noise before rounding up, so it can only ever lower a charge — by 1 credit, in rare cases.
- Changed: a model whose engine documents no reference-image limit publishes
capabilities.reference_images.max: 16(the request ceiling) instead ofreference_images: null; no published limit exceeds 16. - Changed:
sa-video-pro/sa-video-fastare now named "Seedance 2.0" / "Seedance 2.0 Fast" (nameanddescriptionchanged; the ids did not). - Changed: a resolution priced at
0credits no longer exists. Every listed model has a positive rate at every resolution andminimum_chargeis always1(the previous "a resolution whose rate is0is free" rule never applied to a published model). - Tightened:
seedmust be0–2147483647(every seed-capable engine's range; outside it the job used to be charged and then fail at the engine).sa-video-*do not acceptseedat all, as before. - An
Idempotency-Keyretry now returns the original job even when the model has since been withdrawn or re-priced, instead of answeringunknown_model. - New: a one-time welcome bonus (currently 20 credits) credited when an account is issued an API key — once per account, not per key. See Billing.
- New: self-serve top-ups by card at https://seedagent.ai/api-access/top-up for accounts that own an API key, with a one-time top-up bonus (see 2026-09-28 (later the same day) for the current terms). See Top-ups.
- New:
GET /v1/balancereturnstop_up_url, the top-up page's address (additive).
2026-09
Every change below is additive except where marked tightened. No submission had rendered before this revision, so no existing job is affected.
POST /v1/videosnow hands the job to the engine inside the request (before, submissions never started). The202status is thereforeprocessing, the body is the full video object (video_url,error,completed_atadded), and the call can take longer — see the timeout advice above.GET /v1/videos/{id}and webhookdatanow includeaspect_ratioandresolution.- New:
Idempotency-Key,GET /v1/videos(list),GET /v1/balance,X-Request-Id/error.request_id,X-RateLimit-*headers, separate read and write rate-limit buckets,error.reasonon failed jobs, error codescontent_policy_violation,idempotency_key_in_use,idempotency_key_reused. - New catalogue fields:
capabilities.seed,capabilities.negative_prompt,constraints.duration.default,constraints.default_resolution,pricing.credits_per_second_by_resolution,pricing.rounding,pricing.minimum_charge. Prices now track live rates (they were a fixed table that under-quoted 4k). - Webhooks: per-key
v2signature; stableX-Seedagent-Delivery/ eventidacross redeliveries; retries for ~21 h instead of ~5 s; redirects are no longer followed (tightened). resolution/aspect_ratioare matched case-insensitively and echoed in the catalogue's spelling (1080P→1080p).sa-video-prois served by a different engine route; its published constraints changed accordingly (min_image_sizeis now 300×300).- Tightened validation: one input mode per request; image fields must be
http(s)URLs ≤ 2048 characters;webhook_urlmust behttpson a public hostname (no IP addresses, no credentials);metadata≤ 50 keys, keys ≤ 40 characters;seed/negative_promptare rejected by models that ignore them;model≤ 64 characters. - A failed API-key lookup on our side now answers
500 internal_error(withRetry-After) instead of401 invalid_api_key.