Perseworks PERSEWORKS
On this page
Getting started

Introduction

The Perseworks API gives you Seedance video, Seedream image, and Doubao Seed chat models through one key and one prepaid balance. It is organised around REST: JSON request bodies, JSON responses, standard HTTP status codes, and Bearer-token authentication.

Chat is OpenAI-compatible — if you already use an OpenAI SDK, change only base_url and the API key. Video and image endpoints follow the same conventions.

All requests must use HTTPS. Requests over plain HTTP, or without a key, fail.

Base URL
https://api.perseworks.hk/v1
OpenAI SDK
from openai import OpenAI

client = OpenAI(
    api_key="sk-...",
    base_url="https://api.perseworks.hk/v1",  # the only change
)

Before your first request

Most "my key doesn't work" reports are one of these five steps. Do them in order:

  1. Sign in at app.perseworks.hk with your email code.
  2. Add points. New accounts start with a balance of 0. Every request reserves points before it runs, so with an empty balance every call returns 402 insufficient_points — even with a valid key. Buy a point pack on Billing, or redeem a code there.
  3. Create a key on API Keys. The full sk-… value is shown once — copy it then. Leave Allowed models and IP allowlist empty unless you need them.
  4. Use the right base URL: https://api.perseworks.hk/v1. Requests must go to api.perseworks.hk — not perseworks.hk (the website) or app.perseworks.hk (the dashboard). A key sent to the wrong host never reaches the API, so you'll see no error from us and no usage in the dashboard.
  5. Run the smoke test on the right. The first call checks connectivity; the second checks your key and balance, and costs a fraction of a point.

Still failing? Send the full HTTP status and JSON error body to support@perseworks.hk, together with the first 11 characters of your key (sk- plus 8) — never the whole key.

Request
# 1 · reachability — no key needed
curl https://api.perseworks.hk/v1/models

# 2 · your key + balance — the cheapest possible call
curl https://api.perseworks.hk/v1/chat/completions \
  -H "Authorization: Bearer sk-..." \
  -H "Content-Type: application/json" \
  -d '{"model":"seed-2.0-mini","max_tokens":16,
       "messages":[{"role":"user","content":"ping"}]}'
Response — empty balance
HTTP/1.1 402 Payment Required

{
  "error": {
    "code": "insufficient_points",
    "message": "Not enough points for this request",
    "required": 1060
  }
}

Authentication

Authenticate every request with an API key in the Authorization header:

Authorization: Bearer sk-…

Keys carry the full spending power of your balance. Keep them server-side — never in a browser, mobile app, or public repo. If a key leaks, revoke it on the API Keys page and create a new one; revocation is immediate.

Each key can optionally be restricted:

  • Allowed models — calls to any other model return 403 model_not_allowed.
  • IP allowlist — matched against the public egress IP of the machine making the call (not your laptop's LAN address). Other sources get 403 ip_not_allowed.

GET /v1/models is public and needs no key.

Request
curl https://api.perseworks.hk/v1/chat/completions \
  -H "Authorization: Bearer sk-..." \
  -H "Content-Type: application/json" \
  -d '{"model":"seed-2.0-mini","messages":[{"role":"user","content":"ping"}]}'
Response — bad key
HTTP/1.1 401 Unauthorized

{
  "error": {
    "code": "invalid_api_key",
    "message": "Unknown or revoked API key"
  }
}

Errors

Perseworks uses conventional HTTP status codes: 2xx success, 4xx a problem with the request (fix it before retrying), 5xx a problem on our side or the model provider's (safe to retry with backoff). Every error has the same JSON shape.

A request that fails is never charged: any points reserved for it are returned.

StatusCodeMeaning
400invalid_requestThe body is not valid JSON or a required field is missing / out of range.
400unknown_modelThe model id doesn't exist or doesn't fit this endpoint (e.g. a video model on /chat/completions).
400unsupported_resolutionThe model does not offer that resolution.
400invalid_durationdurationSec exceeds the model's maximum.
400too_many_references too_many_reference_videos too_many_reference_audios too_many_imagesMore reference media than the model accepts.
400input_rejectedThe model provider refused an input (e.g. a reference image showing a real person). Change the input — retrying won't help. Not charged.
401invalid_api_keyMissing, malformed, unknown, or revoked API key.
402insufficient_pointsYour balance can't cover the reservation. required is the amount needed. Top up and retry.
403model_not_allowedThis key is restricted to other models.
403ip_not_allowedThis key has an IP allowlist and the request came from another address.
404not_foundThe task doesn't exist or belongs to another account.
413payload_too_largeBody over the size cap (chat 8 MB · video 100 KB).
422content_rejectedThe prompt failed content moderation. Not charged.
429rate_limitedToo many requests for this key. Wait for Retry-After seconds.
500internal_errorSomething went wrong on our side. Any reservation is refunded.
502upstream_errorThe model provider failed or timed out. Any reservation is refunded; safe to retry.

Attributes

  • error.code string
    Machine-readable code from the table below. Branch on this, not on the message.
  • error.message string
    Human-readable explanation. May change; do not parse.
  • error.required number
    Only on insufficient_points: points needed to reserve the request.
Error object
{
  "error": {
    "code": "model_not_allowed",
    "message": "API key not scoped for model \"seedance-2.0\""
  }
}

Points & billing

Usage is paid from a prepaid points balance — 13,800 points = US$1. One balance covers every model.

  1. Reserve. Before running, the request reserves its maximum possible cost (for chat: prompt + max_tokens; for video: durationSec × per-second rate). If the balance can't cover it you get 402.
  2. Settle. On success you are charged the actual usage — real tokens, the real video length — and the rest of the reservation is released.
  3. Refund. On any failure the full reservation is returned.

Successful responses report the charge in the X-Credits-Charged header and in usage.credits (both in points). Live per-model prices are always in GET /v1/models; see also the pricing page.

Tip: a large max_tokens on chat raises the reservation, not the bill — but it can trigger 402 on a low balance. Set it to what you actually need.

Response headers — success
HTTP/1.1 200 OK
X-Credits-Charged: 1485
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119
Response — insufficient balance
HTTP/1.1 402 Payment Required

{
  "error": {
    "code": "insufficient_points",
    "message": "Not enough points for this request",
    "required": 1060
  }
}

Rate limits

Limits are applied per API key, separately for each endpoint family: 120 requests per minute each for /v1/chat, /v1/images, and /v1/video (polling counts against the video budget — poll every 5–10 s, not in a tight loop).

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (seconds). Over the limit you receive 429 rate_limited with a Retry-After header — wait that many seconds, then retry.

Need more throughput? Contact support@perseworks.hk.

Response — rate limited
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0

{ "error": { "code": "rate_limited", "message": "too many requests" } }

Idempotent requests

Video jobs are expensive, so POST /v1/video/generations supports idempotency: send an Idempotency-Key header and a retry with the same key returns the original task instead of starting (and charging for) a second one.

Use a random value such as a UUID v4, generated once per logical job and reused on every retry of that job — for example after a network timeout where you can't tell whether the first submit landed.

Request
curl https://api.perseworks.hk/v1/video/generations \
  -H "Authorization: Bearer sk-..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f1d6c1e-9b0a-4c36-8d1e-2a7f3c9b4e10" \
  -d '{"model":"seedance-2.0-fast","prompt":"a cat surfing a wave, cinematic","resolution":"720p","durationSec":5}'
Models

The model object

Describes one callable model and its live price. Exactly one of the pricing fields is present, depending on category.

Attributes

  • id string
    The value you pass as model.
  • name string
    Display name.
  • category enum
    video · image · chat
  • status string
    active for callable models.
  • billing_unit enum
    per_second · per_image · per_1m_token
  • pricing array
    Video only. One entry per supported resolution.
    pricing ▸
    • resolution string
      720p · 1080p · 4k
    • points_per_sec number
      Points per output second.
    • usd_per_sec number
      Same price in USD.
  • image_pricing object
    Image only.
    image_pricing ▸
    • points_per_image number
      Points per generated image.
    • usd_per_image number
      Same price in USD.
    • extra_input_image_points number
      Surcharge per reference image beyond the first.
  • token_pricing object
    Chat only.
    token_pricing ▸
    • points_per_1m_input number
      Points per million input tokens.
    • points_per_1m_output number
      Points per million output tokens.
    • points_per_1m_cached_input number
      Points per million cached input tokens.
    • context_window number
      Maximum context in tokens.
The model object
{
  "id": "seedance-2.0",
  "name": "Seedance 2.0",
  "category": "video",
  "status": "active",
  "billing_unit": "per_second",
  "pricing": [
    { "resolution": "720p",  "points_per_sec": 2760,  "usd_per_sec": 0.2  },
    { "resolution": "1080p", "points_per_sec": 6210,  "usd_per_sec": 0.45 },
    { "resolution": "4k",    "points_per_sec": 13800, "usd_per_sec": 1    }
  ]
}

List models

GET /v1/models

Returns every model you can call, with live prices. Public — no API key required. Current models:

Model idTypeNotes
seedance-2.5videoFlagship. 720p · 4–30 s · text / image / video / audio references · edit & extend.
seedance-2.0video720p · 1080p · 4k · up to 15 s · native audio.
seedance-2.0-fastvideo720p · faster turnaround.
seedance-2.0-minivideo720p · lowest cost.
seedream-5.0imageSeedream 5.0 Pro · text-to-image + editing · flat per-image rate.
seed-2.1-prochatStrongest chat model · vision · 262K context.
seed-2.0-minichatFast and low cost · 262K context.

Returns

A list object whose data is an array of model objects.
GET /v1/models
curl https://api.perseworks.hk/v1/models
Response
{
  "object": "list",
  "data": [
    { "id": "seedance-2.5", "category": "video", ... },
    { "id": "seedance-2.0", "category": "video", ... },
    { "id": "seedream-5.0", "category": "image",
      "billing_unit": "per_image",
      "image_pricing": { "points_per_image": 1485, "usd_per_image": 0.1076,
                         "extra_input_image_points": 49 } },
    { "id": "seed-2.1-pro", "category": "chat",
      "billing_unit": "per_1m_token",
      "token_pricing": { "points_per_1m_input": 15220,
                         "points_per_1m_output": 76103,
                         "points_per_1m_cached_input": 3044,
                         "context_window": 262144, ... } },
    ...
  ]
}
Chat

Create a chat completion

POST /v1/chat/completions

Generates a model response for a conversation. Request and response follow the OpenAI Chat Completions format, so OpenAI SDKs work unchanged.

Parameters

  • model string Required
    seed-2.1-pro or seed-2.0-mini.
  • messages array Required
    The conversation so far, oldest first. 1–256 messages.
    messages ▸
    • role enum Required
      system · user · assistant
    • content string | array Required
      Text (up to 128K characters), or an array of content parts to send images — see Send images.
  • stream boolean
    If true, the response is streamed as server-sent events, ending with data: [DONE]. Default false.
  • max_tokens integer
    Maximum tokens to generate, 1–8192. Default 1024. Also sizes the point reservation.
  • response_format object
    {"type": "json_object"} forces valid JSON output; {"type": "text"} is the default.
  • thinking object
    {"type": "disabled"} turns off deep thinking for lower latency; enabled / auto also accepted.

Returns

A chat.completion object. With stream: true, a sequence of chat.completion.chunk events. usage.credits is the number of points charged.
POST /v1/chat/completions
curl https://api.perseworks.hk/v1/chat/completions \
  -H "Authorization: Bearer sk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "seed-2.1-pro",
    "messages": [{"role": "user", "content": "Hello!"}],
    "stream": true
  }'
Response
{
  "id": "chatcmpl-x2Lq9ZtR0bWk4mNv8sYe",
  "object": "chat.completion",
  "created": 1785000000,
  "model": "seed-2.1-pro",
  "choices": [{
    "index": 0,
    "message": { "role": "assistant", "content": "Hi! How can I help?" },
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 8,
    "total_tokens": 17,
    "credits": 1
  }
}
Response — stream: true
data: {"id":"chatcmpl-…","object":"chat.completion.chunk",
       "choices":[{"index":0,"delta":{"content":"Hi"},"finish_reason":null}]}

data: {"id":"chatcmpl-…","object":"chat.completion.chunk",
       "choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]

Send images

POST /v1/chat/completions

seed-2.1-pro can read images. Pass content as an array of parts instead of a string.

Content parts

  • { type: "text" } object
    A text part: {"type": "text", "text": "…"}.
  • { type: "image_url" } object
    An image part.
    { type: "image_url" } ▸
    • image_url.url string Required
      An HTTPS URL, or a base64 data URI (data:image/jpeg;base64,…).
    • image_url.detail enum
      low bills each image as 512 input tokens; high (default) as 1,600.

Returns

Limits: up to 32 images per request, 64 parts per message, 8 MB total body.
POST /v1/chat/completions
curl https://api.perseworks.hk/v1/chat/completions \
  -H "Authorization: Bearer sk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "seed-2.1-pro",
    "messages": [{
      "role": "user",
      "content": [
        {"type": "text", "text": "What is in this image?"},
        {"type": "image_url", "image_url": {"url": "https://example.com/cat.jpg", "detail": "low"}}
      ]
    }]
  }'
Images

Create an image

POST /v1/images/generations

Generates images from a prompt, optionally guided by reference images. The call is synchronous and typically takes 70–90 seconds — set your HTTP client timeout to at least 120 s. (If your own server sits behind a proxy with a ~100 s limit, such as Cloudflare, call this from a background job.)

Parameters

  • model string Required
    seedream-5.0
  • prompt string Required
    What to generate. Up to 4,000 characters.
  • size enum
    1K or 2K. Default 2K. Same price for both.
  • image string | array
    Reference image(s) for editing or multi-reference generation: URL or base64, up to 10.
  • n integer
    Number of images, 1–4. Default 1. Billed per image.
  • output_format enum
    png · jpeg
  • seed integer
    Fix for reproducible output.

Returns

An object whose data is an array of { url }. Download and store the images you want to keep.
POST /v1/images/generations
curl https://api.perseworks.hk/v1/images/generations \
  -H "Authorization: Bearer sk-..." \
  -H "Content-Type: application/json" \
  --max-time 150 \
  -d '{
    "model": "seedream-5.0",
    "prompt": "a red panda barista, cinematic lighting",
    "size": "2K"
  }'
Response
{
  "id": "img_4nT8vQ1sXw9cR2kL",
  "created": 1785000000,
  "model": "seedream-5.0",
  "data": [ { "url": "https://.../out.png" } ],
  "usage": { "images": 1, "credits": 1485 }
}
Video

The video task object

Video generation is asynchronous. Submitting returns a task in queued state; poll it until it reaches succeeded or failed.

Attributes

  • id string
    Task id, prefixed vid_.
  • status enum
    queued → running → succeeded | failed
  • model string
    Returned on submit.
  • video_url string
    When succeeded: the MP4. Download promptly.
  • thumbnail_url string
    When succeeded: a cover image.
  • duration number
    When succeeded: actual length in seconds — what you are billed for.
  • error object
    When failed: { code, message }. The reservation is refunded.
Succeeded
{
  "id": "vid_8KqZ2mR4tY7wP1xN",
  "status": "succeeded",
  "video_url": "https://.../vid_8KqZ2mR4tY7wP1xN.mp4",
  "thumbnail_url": "https://.../vid_8KqZ2mR4tY7wP1xN.jpg",
  "duration": 5
}
Failed
{
  "id": "vid_8KqZ2mR4tY7wP1xN",
  "status": "failed",
  "error": { "code": "upstream_failed", "message": "Video generation failed" }
}

Create a video

POST /v1/video/generations

Starts a video job and returns immediately with 202 Accepted. Points for durationSec are reserved now and settled against the actual output length. Supports the Idempotency-Key header.

Parameters

  • model string Required
    seedance-2.5 · seedance-2.0 · seedance-2.0-fast · seedance-2.0-mini
  • prompt string Required
    Up to 2,000 characters.
  • resolution enum Required
    720p · 1080p · 4k (lower-case). 1080p and 4k on seedance-2.0 only; every other model is 720p.
  • durationSec integer Required
    Seconds. seedance-2.5: 4–30. Others: 1–15.
  • aspectRatio enum
    16:9 · 4:3 · 1:1 · 3:4 · 9:16 · 21:9 · adaptive
  • image string
    Image URL to animate (image-to-video).
  • firstFrame string
    Image URL to use as the first frame.
  • lastFrame string
    Image URL to use as the last frame.
  • references array
    Reference image URLs. seedance-2.5: up to 30. Others: up to 9.
  • referenceVideos array
    Reference video URLs. seedance-2.5: up to 4. Others: up to 3.
  • referenceAudios array
    Reference audio URLs, up to 2.
  • seed integer
    Fix for reproducible output.

Returns

202 with a video task in queued state. Media URLs must be publicly reachable by the model provider.
POST /v1/video/generations
curl https://api.perseworks.hk/v1/video/generations \
  -H "Authorization: Bearer sk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "seedance-2.0",
    "prompt": "a cat surfing a wave, cinematic",
    "resolution": "1080p",
    "durationSec": 5,
    "aspectRatio": "16:9"
  }'
Response
HTTP/1.1 202 Accepted

{
  "id": "vid_8KqZ2mR4tY7wP1xN",
  "status": "queued",
  "model": "seedance-2.0"
}

Retrieve a video

GET /v1/video/generations/{id}

Returns the current state of a task you created. Poll every 5–10 seconds until status is succeeded or failed.

Path parameters

  • id string Required
    The vid_… id returned on create.

Returns

A video task. 404 not_found if the id is unknown or owned by another account.
GET /v1/video/generations/{id}
curl https://api.perseworks.hk/v1/video/generations/vid_8KqZ2mR4tY7wP1xN \
  -H "Authorization: Bearer sk-..."
Response
{
  "id": "vid_8KqZ2mR4tY7wP1xN",
  "status": "succeeded",
  "video_url": "https://.../vid_8KqZ2mR4tY7wP1xN.mp4",
  "thumbnail_url": "https://.../vid_8KqZ2mR4tY7wP1xN.jpg",
  "duration": 5
}