Video

Text-to-video and image-to-video through domestic and overseas channels. Use the same endpoint and video-enabled API key, and select the channel in each request.

Available by arrangement, not yet self-serve. A key has to be turned on for video before it can reach these routes. One that has not been gets 403 permission_error. Keep your existing video-enabled key for either channel. Contact us if video is not enabled.

Endpoint

Base URLhttps://api.nightferry.io
CreatePOST /api/v3/contents/generations/tasks
PollGET /api/v3/contents/generations/tasks/{id}
Generation recordGET /api/v3/contents/generations/tasks/{id}/record
Auth headerAuthorization: Bearer <your key>
Task id prefixcgt-

x-api-key works too, for consistency with the text API.

Domestic and overseas channels

The overseas channel supports NSFW. To test it, explicitly set "channel": "overseas" in the create request. Keep your existing API key; no additional NSFW parameter is needed.

ChannelRequest field
Domestic"channel": "domestic"
Overseas, with NSFW support"channel": "overseas"

Omitting channel selects domestic. Both channels use the same public model IDs, endpoint, key and fixed USD selling prices. Polling needs no channel parameter; check channel in the task response.

There is no automatic channel fallback. If the selected model lacks an overseas route or an applicable price, creation returns 409 pricing_unavailable before generation. Use different Idempotency-Key values for tests on different channels.

Models

Limits per model
Model idResolutionDurationImagesVideosAudio
doubao-seedance-2-0-260128480p / 720p / 1080p / 4k4–15 or −1933
doubao-seedance-2-0-fast-260128480p / 720p / 1080p4–15 or −1933
doubao-seedance-2-0-mini-260615480p / 720p / 1080p4–15 or −1933
doubao-seedance-2-5-260628480p / 720p / 1080p4–30 or −1301010

doubao-seedance-2-0-fast-260128 is the default if you send no model. 4k is available on doubao-seedance-2-0-260128 only. duration: -1 lets the model choose its own length.

Create a task

Set NIGHTFERRY_KEY to your existing key. Both examples use Mini, 480p, 4 seconds and no audio to make requests and charges easy to compare. Use a new idempotency key for each new test; keep it when retrying the same task.

Domestic

curl -X POST https://api.nightferry.io/api/v3/contents/generations/tasks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $NIGHTFERRY_KEY" \
  -H "Idempotency-Key: video-domestic-text-001" \
  -d '{
  "model": "doubao-seedance-2-0-mini-260615",
  "channel": "domestic",
  "content": [
    {
      "type": "text",
      "text": "a cat running through long grass, cinematic"
    }
  ],
  "resolution": "480p",
  "ratio": "16:9",
  "duration": 4,
  "generate_audio": false
}'

Overseas

curl -X POST https://api.nightferry.io/api/v3/contents/generations/tasks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $NIGHTFERRY_KEY" \
  -H "Idempotency-Key: video-overseas-text-001" \
  -d '{
  "model": "doubao-seedance-2-0-mini-260615",
  "channel": "overseas",
  "content": [
    {
      "type": "text",
      "text": "a cat running through long grass, cinematic"
    }
  ],
  "resolution": "480p",
  "ratio": "16:9",
  "duration": 4,
  "generate_audio": false
}'

Successful creation returns a task ID before the video is ready. An accepted overseas task looks like this:

{
  "id": "cgt-4f9a72b7476bc7838a470c3df57258da",
  "model": "doubao-seedance-2-0-mini-260615",
  "status": "queued",
  "channel": "overseas",
  "billing": {
    "currency": "USD",
    "state": "held",
    "charge_usd": null,
    "billable_tokens": null,
    "backfilled": false
  }
}

content takes text, image_url, video_url and audio_url items, each with an optional role of first_frame, last_frame, reference_image, reference_video or reference_audio. At least one text, image_url or video_url is required — audio on its own is refused.

Also accepted: ratio, duration, generate_audio, watermark, seed, tools, safety_identifier, bitrate_mode, output_format, omni_reference_task_type and moderation_options.ips.

Using a reference image

Replace the example image URL with an HTTPS image URL the service can read. reference_image supplies an image reference; use first_frame to set the opening frame.

curl -X POST https://api.nightferry.io/api/v3/contents/generations/tasks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $NIGHTFERRY_KEY" \
  -H "Idempotency-Key: video-overseas-image-001" \
  -d '{
  "model": "doubao-seedance-2-0-mini-260615",
  "channel": "overseas",
  "content": [
    {
      "type": "text",
      "text": "a cat running through long grass, cinematic"
    },
    {
      "type": "image_url",
      "image_url": {
        "url": "https://example.com/reference.jpg"
      },
      "role": "reference_image"
    }
  ],
  "resolution": "480p",
  "ratio": "16:9",
  "duration": 4,
  "generate_audio": false
}'

Where we differ from Ark

There are no webhooks. callback_url is accepted and does nothing. Poll.

Not buying the same video twice

Send an Idempotency-Key header. A repeat carrying the same key returns the original task instead of creating a second one. Switching channels with that key returns 409 idempotency_conflict. Changing other parameters does not update the original task. Use a different Idempotency-Key to request another video from the same prompt.

Two requests with the same key arriving at the same instant are settled before either is charged: one wins, the other is told. If the winner has not finished registering yet, the loser gets 409 conflict — poll the task you already have, or retry.

Without the header we still catch a repeat with the same normalized request from the same key within a short window. Use the header; it is exact.

Poll a task

curl https://api.nightferry.io/api/v3/contents/generations/tasks/cgt-4f9a72b7476bc7838a470c3df57258da \
  -H "Authorization: Bearer $NIGHTFERRY_KEY"

Replace the ID in the URL with the create response’s id. Poll no more often than once every 5 seconds. HTTP 200 means the lookup succeeded; check the task’s status too. Wait on queued or running; succeeded means the video is delivered and failed means the task failed.

Delivered and charged

{
  "id": "cgt-4f9a72b7476bc7838a470c3df57258da",
  "model": "doubao-seedance-2-0-mini-260615",
  "status": "succeeded",
  "channel": "overseas",
  "billing": {
    "currency": "USD",
    "state": "settled",
    "charge_usd": "0.137303",
    "billable_tokens": 40594,
    "backfilled": false
  },
  "content": {
    "video_url": "https://api.nightferry.io/v1/videos/cgt-4f9a72b7476bc7838a470c3df57258da.mp4"
  },
  "resolution": "480p",
  "duration": 4,
  "usage": {
    "completion_tokens": 40594,
    "total_tokens": 40594
  }
}

Accepted, then failed

This poll response still has HTTP status 200. Read status: failed and error to detect failure. billing.state: released means the hold was released without a generation charge. Customer error codes do not identify the exact moderation stage.

{
  "id": "cgt-4f9a72b7476bc7838a470c3df57258da",
  "model": "doubao-seedance-2-0-mini-260615",
  "status": "failed",
  "channel": "overseas",
  "billing": {
    "currency": "USD",
    "state": "released",
    "charge_usd": null,
    "billable_tokens": null,
    "backfilled": false
  },
  "error": {
    "code": "content_policy_violation",
    "message": "The request or its output did not pass content safety checks."
  }
}
There is no cancel. A submitted task runs to completion or to its expiry. Nothing stops it in between, so check the request before you send it.

The usage block has no input count

completion_tokens and total_tokens are both output-side. There is no prompt or input token count for video. The two are usually equal but are not promised to be, which is why both are reported.

The video

content.video_url is on our own domain and needs no API key — it is meant to be handed to a browser or a player. Treat the URL itself as the credential: anyone holding it can watch the video, so do not publish it anywhere you would not publish the video.

We keep it for 7 days. After that the URL returns 410. Download anything you mean to keep.

Generation records

New API and Video Lab tasks save their original prompt text and generation parameters by default for 30 days from task creation. A failed task still keeps its inputs. Retrying a task does not replace its record or extend its deadline.

curl https://api.nightferry.io/api/v3/contents/generations/tasks/cgt-4f9a72b7476bc7838a470c3df57258da/record \
  -H "Authorization: Bearer $NIGHTFERRY_KEY"

Use any valid enabled key belonging to the task's tenant. The response contains the task fields, created_at and inputs. Ordinary create and polling responses do not include saved input text.

FieldMeaning
inputs.stateavailable, expired or never_captured.
inputs.expires_atUTC ISO expiry timestamp; null when inputs were never saved.
inputs.promptsOriginal text strings in submitted order, including whitespace. An empty array means the request had no text.
inputs.submittedSubmitted public parameters, including accepted but ignored settings.
inputs.effectivePublic settings after defaults and limits were applied. Ignored settings are absent.
inputs.referencesReference metadata: type, role and count. No reference files or access URLs are retained.

Only available records include the four content fields. At 30 days, they are removed from record reads and prompt search, and the saved content is cleaned up. Task and billing information remain. Older tasks whose inputs were not saved return never_captured; their prompts cannot be reconstructed.

In Console, click a task ID in Video billing, look up a task ID, or search retained prompt content. Search matches literal Chinese or English fragments without case sensitivity; queries may contain up to 500 characters. You can copy the prompt and parameters. Console history belongs to the signed-in tenant and does not require the original creation key.

The generated video still has its separate 7-day lifetime. The record plays it while available and shows expiry or playback errors separately. Prompt history can remain after the video expires. Reference files remain disposable inputs; history does not archive them or provide one-click regeneration.

Errors

The table covers HTTP errors when creation or the lookup itself fails. A generation failure on an accepted task is returned with HTTP 200, as shown above. Check the HTTP status, task status and error.code together.

HTTPerror.codeMeaning
400invalid_request_errorRejected. error.param names the field.
400content_policy_violationInput, or the generated output, failed content safety.
401authentication_errorKey missing or invalid.
402insufficient_creditInsufficient available account credit.
403permission_errorThis key is not enabled for video.
404not_foundNo such task, or no such video.
409conflictAn identical request is already in flight.
409idempotency_conflictAn idempotency key was reused on another channel.
409pricing_unavailableThe selected route or configuration price is unavailable. Contact us to confirm availability.
410goneThe video existed and has passed its 7-day retention.
413invalid_request_errorThe request body is too large.
429video_too_many_in_flightToo many tasks running at once.
429rate_limit_exceededSlow down and retry.
502upstream_errorThe video service is temporarily unavailable. Keep the task ID and contact us; replacing your key will not resolve this error.
503capacity_unavailableVideo capacity is temporarily unavailable.
When too many tasks are running, retry after one finishes. For rate-limit errors, slow down before retrying.

Usage

curl "https://api.nightferry.io/v1/usage/video?since=2026-09-01&until=2026-09-05" \
  -H "Authorization: Bearer $NIGHTFERRY_KEY"

Returns video usage for the calling key, grouped by model, resolution, channel and reference-video category. Successful and failed task counts and both token counters are separate. Dates use UTC and YYYY-MM-DD; until is exclusive. The default window covers the last 30 days, including today. Adjust the example dates to cover your tests.

USD billing and checking charges

The price columns are alternatives: use “with reference video” if the request contains a video reference; otherwise use “no reference video”. The bill is validated completion_tokens × the task’s frozen USD price per million ÷ 1,000,000, rounded half up to six USD decimals. total_tokens is retained separately and is not added. For example, Mini 480p without a reference video, with 40,594 completion tokens, costs $0.137303.

The account shares one balance across text, video and all keys. Creating a task holds credit; a delivered success with verified usage posts its charge and releases the hold. A definitive failure releases it without a success charge. Missing usage, an unknown outcome or an interrupted video copy stays pending reconciliation, with credit held. Polling and Console remain available while in debt. New video calls use available account credit, which must cover the request hold; insufficient credit returns 402 insufficient_credit.

Task responses include channel and billing: currency, state (held, pending, settled or released), charge_usd, billable_tokens and backfilled. A null charge in held or pending is unresolved, not free. In released, the hold was released without a generation charge. Historical successful work is billed when its ownership, usage and category are supported. A backbill can produce a negative balance.

GET /v1/usage/video groups by model, resolution, channel and reference-video category. Each group and the totals include pending, billable_tokens and charge_usd; pending counts held and pending-reconciliation tasks, and charges include only posted amounts as USD strings with six decimals. GET /v1/usage shows the shared account balance, held and available credit.

Confirm the applicable fixed USD price with us before using Mini/Fast 1080p. Until configured, it returns 409 pricing_unavailable before generation.

Testing and acceptance

If a result differs from what you expect, retain the task ID, model, channel, request time, HTTP status, task status and error code so we can investigate. Do not include your API key.