Docsfrom key to first clip
Everything you need for a first render: a key, one request, the job, and the webhook.
Developer API access is by application.
Overview
REST over HTTPS, JSON in and out. Every generation is asynchronous.
- Base URL
https://api.pershot.app/v1- Authentication
- Authorization: Bearer followed by your key. Keys start with sk_test_ or sk_live_ and are shown once; we store only a hash.
- Key permissions
- Each key has scopes and, if you set one in the studio, an allowlist of models and tools. A key with an allowlist can use only what is on it, tools included: image upscale, background removal, Enhance, lip sync, outpaint and the image editor. Anything else answers 403 forbidden_model. 1080p or 4K with Enhance needs both the video model and Enhance.
- Error shape
{ "error": { "code", "message", "param", "request_id" } }- IDs
- Prefixed by type: job_, qt_, key_, up_.
- OpenAPI
- /api/v1/openapi.json
- For AI agents
- Plain text for AI agents: inputs, legal values, every price and a request example
seedance-2.0-fastseedance-2.0-miniqwen-image-2512-lightningqwen-image-2512qwen-image-edit-2511-lightningqwen-image-edit-2511
Real renders are not open yet. Leave your email on a model page and we write once, when they open.
Quickstart
Four steps, each with the response you should see.
- 01
Get a key
Developer API access is by application: request it under API keys in the studio. Once it is open, create a key there and copy it: the full key is shown once.
export API_KEY="sk_test_…" - 02
Send a request
POST the shot with an Idempotency-Key and a max_credits cap. You get 202 and a queued job straight away, and the credits are held.
curl https://api.pershot.app/v1/videos/generations \ -H "Authorization: Bearer $API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.0-fast", "prompt": "Perfume mist in hard side light. Slow push-in, black background, grain.", "duration": 5, "resolution": "720p", "aspect_ratio": "16:9", "generate_audio": true, "max_credits": 34 }'{ "id": "job_01J9ZK4T8W", "object": "job", "kind": "video", "status": "queued", "money_status": "reserved", "queue_position": 2, "requested_model": "seedance-2.0-fast", "credits_quoted": 34, "credits_charged": null } - 03
Check the job
GET the job until its status is succeeded. The output URL is signed and expires; download what you keep.
curl https://api.pershot.app/v1/jobs/job_01J9ZK4T8W \ -H "Authorization: Bearer $API_KEY"{ "id": "job_01J9ZK4T8W", "object": "job", "kind": "video", "status": "succeeded", "money_status": "settled", "requested_model": "seedance-2.0-fast", "resolved_model": "seedance-2.0-fast", "credits_quoted": 34, "credits_charged": 34, "outputs": [ { "kind": "video", "url": "https://…/job_01J9ZK4T8W.mp4?sig=…", "width": 1280, "height": 720, "duration_s": 5 } ], "error": null } - 04
Receive the webhook
Pass webhook_url and we POST every state change to it, signed with a timestamp. Answer 2xx; failed deliveries retry with backoff.
{ "id": "evt_01J9ZKB1QX", "type": "job.succeeded", "created_at": "2026-09-29T14:02:31Z", "data": { "job": { "id": "job_01J9ZK4T8W", "object": "job", "status": "succeeded", "money_status": "settled", "credits_charged": 34, "outputs": [ { "kind": "video", "url": "https://…/job_01J9ZK4T8W.mp4?sig=…", "width": 1280, "height": 720, "duration_s": 5 } ] } } }
Endpoints
All paths are relative to the base URL.
| Method | Path | What it does |
|---|---|---|
| GET | /v1/models | Models, capabilities and status available to your account |
| GET | /v1/pricing | Public prices; logged in, your account's prices |
| POST | /v1/quotes | Plan, line items, maximum charge and expiry (10 minutes) |
| POST | /v1/videos/generations | Generate a video; 202 and a job |
| POST | /v1/images/generations | Generate or edit an image; 202 and a job |
| GET | /v1/jobs/{id} | Status, outputs as signed links, actual cost |
| GET | /v1/jobs | Your jobs, newest first |
| POST | /v1/jobs/{id}/cancel | Ask to cancel; the held credits are refunded unless the model has already accepted the job |
| GET | /v1/balance | Available and held credits, by bucket |
| GET | /v1/usage | Usage by day or model, as JSON or CSV |
| POST | /v1/uploads | Upload a JPEG, PNG or WebP up to 10 MB |
Quotes and caps
Every generation needs either a quote_id or a max_credits cap, never both. With a quote_id the job runs at that quote; with max_credits it runs only if the price is at or below the cap.
model: "auto" always needs max_credits. A quote is valid for 10 minutes; after that you get quote_expired.
Idempotency
Send an Idempotency-Key header (1 to 255 visible ASCII characters) on every generation request.
The same key within 24 hours returns the same job with HTTP 200 instead of 202, so retrying after a timeout never creates or charges a second job.
Webhooks
Pass webhook_url on a generation (or on a batch) to receive its state changes. Each event is a POST whose JSON body has id, type, created_at and data: data.job for job events, data.batch for batch.completed.
Events
job.queuedjob.processingjob.succeededjob.failedjob.cancelledbatch.completed
- Each delivery has an event id, a timestamp and a signature. Verify it before you trust the body.
- A delivery counts when your endpoint answers 2xx within 10 seconds. Otherwise it is retried after 1 min, 5 min, 30 min, 2 h and 6 h, then marked failed; support can replay it. Redirects are not followed.
- Webhook URLs must be https. Hosts on private, loopback or reserved addresses are refused, both when you submit and when we connect.
Headers
X-Signaturet=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>X-Event-Id- The event id, also in the body. Retries and replays keep it, so store it and skip an id you have already handled.
- Signing secret
- One whsec_… secret per account, in the studio under API keys. The HMAC key is the whole secret, prefix included.
- Rotation
- After you rotate, the old secret keeps signing for 24 hours as a second v1 entry. Accept a delivery if any v1 matches.
Compute the HMAC over the raw body, not a re-serialized copy, compare in constant time, and refuse timestamps more than five minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received, before any JSON parsing.
export function verifyWebhook(header, rawBody, secret, toleranceSec = 300) {
let t = null;
const signatures = [];
for (const item of (header ?? "").split(",")) {
const [key, value] = item.split("=", 2);
if (key === "t") t = Number(value);
if (key === "v1") signatures.push(Buffer.from(value, "hex"));
}
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return signatures.some((s) => s.length === expected.length && timingSafeEqual(s, expected));
}
// verifyWebhook(req.headers["x-signature"], rawBody, process.env.WEBHOOK_SECRET)
// Answer 2xx quickly, and skip an X-Event-Id you have already handled.Errors
Codes and HTTP statuses are fixed; the message is for people.
| Code | HTTP | When |
|---|---|---|
invalid_request | 400 | A field is missing or out of range |
unauthorized | 401 | No key, a revoked key, or a bad signature |
insufficient_credits | 402 | The balance does not cover the hold |
forbidden_model | 403 | The key's allowlist does not include this model or tool |
region_not_supported | 403 | The model is not offered in your region |
developer_api_closed | 403 | A developer key while developer API access is closed; access is by application |
member_connections_paused | 403 | A member sign-in (MCP, connection code or command line) while connecting your own AI agent is paused |
not_found | 404 | No such job, quote or upload in your account |
quote_expired | 409 | The quote is older than 10 minutes |
price_changed | 409 | The price moved above your cap |
content_rejected | 422 | The content was refused; nothing is charged |
rate_limited | 429 | Too many requests for this key |
concurrency_limited | 429 | Too many jobs running for this account |
internal_error | 500 | Something failed on our side |
upstream_unavailable | 503 | The model is unavailable; retry later |
An uncertain outcome from the model is not an error: the job reports status unknown and is checked before anything is resubmitted.
Limits
- Requests
- 60 per minute per key by default.
- Concurrency
- 3 jobs at once per account by default; ask to raise it.
- Ceiling
- The model's capacity is a hard ceiling. More keys do not add concurrency.
Images
POST /v1/images/generations takes the same headers. Pick a model id by tier and operation; edit models also need image_url, and generate models reject it.
| Tier | generate | edit |
|---|---|---|
| Image MiniEconomy · draft | qwen-image-2512-lightning | qwen-image-edit-2511-lightning |
| Image FastRecommended | qwen-image-2512 | qwen-image-edit-2511 |
Limits by model
Every model's API page has the full table: the value, the unit, who checks it and what happens past it. GET /v1/models returns the same limits as JSON.
- EnforcedWe check it and answer 400 before anything is quoted or held.
- SupplierThe supplier checks it. A job it refuses fails, and nothing is charged.
- RecommendedNobody checks it. Staying inside it gives better results.
| Model | Main limits | Full table |
|---|---|---|
seedance-2.0-fastSeedance 2.0 Fast | Prompt 2 to 2,000 characters · 4 to 15 s · up to 9 reference images · images 300 to 6,000 px per side | Open the table |
seedance-2.0-miniSeedance 2.0 Mini | Prompt 2 to 2,000 characters · 4 to 15 s · up to 9 reference images · images 300 to 6,000 px per side | Open the table |
qwen-image-2512Qwen-Image-2512 | Prompt 2 to 2,000 characters · 1K or 2K · no source image | Open the table |
qwen-image-2512-lightningQwen-Image-2512 · Lightning 4-step | Prompt 2 to 2,000 characters · 1K or 2K · no source image | Open the table |
qwen-image-edit-2511Qwen-Image-Edit-2511 | Prompt 2 to 2,000 characters · 1K or 2K · one source image, JPEG, PNG or WebP up to 10 MB | Open the table |
qwen-image-edit-2511-lightningQwen-Image-Edit-2511 · Lightning | Prompt 2 to 2,000 characters · 1K or 2K · one source image, JPEG, PNG or WebP up to 10 MB | Open the table |
Uploads
POST /v1/uploads reads the type from the file itself. Upload links last 7 days.
| File | Formats | Largest file | Longest |
|---|---|---|---|
| Image: first frame, reference or edit source | JPEG, PNG, WebP | 10 MB | – |
| Video: lip sync | MP4 | 50 MB | 60 s |
| Audio: lip sync | MP3, WAV, M4A | 10 MB | 60 s |
We never fetch images from other hosts, so for those links the supplier checks the size, and a job it refuses is not charged. The same limits come back as JSON: GET /v1/models returns a limits object for every model.
Coming from fal, Replicate or WaveSpeed
Changing the base URL and the key is not enough: every request also needs the model id in the body, an Idempotency-Key header and a price cap (max_credits or a quote_id). The migration guide maps every field for Seedance 2.0 and Qwen-Image and lists what does not carry over.