Pixyn API: generation from your own code
The key gives your code the same set of models, settings and slots that a person sees in the studio, and spends tokens from the same balance. There is no separate plan for access. You issue it in your account: Profile → API. It is shown once.
Getting started
- Issue a key and put it in an environment variable.
- Fetch the catalog: model, setting and slot keys come from there.
- Ask for an estimate: how much your exact request will cost.
- Run the generation and poll the status until the result is ready.
export PIXYN_API_KEY=pxn_live_…The key goes in the Authorization: Bearer … header and acts on your behalf: generations are created on your account and tokens are taken from your balance. By default it can do everything you can do yourself. When issuing it you can narrow the set: for example, a key that can only read the catalog will not spend a single token, even if it is stolen.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /api/v1/me | not needed | Whose key it is, its scopes and limits. |
| GET | /api/v1/models | models:read | Catalog: groups, versions, settings, slots, price. |
| POST | /api/v1/estimate | models:read | How many tokens such a request will cost. |
| POST | /api/v1/generations | generations:create | Run a generation. |
| GET | /api/v1/generations/:id | generations:read | Status and result of one generation. |
| GET | /api/v1/generations | generations:read | List of your API generations. |
Model catalog
It is structured like the picker in the studio: group (brand) → version → input type → settings. Take setting and slot keys only from here. They differ for every model and change along with it.
curl -s "https://pixyn.ai/api/v1/models?category=video" \
-H "Authorization: Bearer $PIXYN_API_KEY"{
"groups": [{
"name": "Kling",
"category": "video",
"variants": [{
"id": "fal-ai-kling-video-v3-pro-image-to-video",
"input_kind": "image2video",
"price": { "base_tokens": 35, "included_seconds": 5, "per_extra_second_tokens": 7 },
"settings": [{ "key": "cfg_scale", "type": "number", "minimum": 0, "maximum": 1, "default": 0.5 }],
"slots": [{ "key": "start_image_url", "type": "frame-start", "required": true }],
"defaults": { "resolution": "1080p" },
"hidden_settings": ["seed"],
"media_tokens": null
}]
}]
}- defaults: model presets. Leave them out and the result will differ from what the site produces.
- hidden_settings: fields hidden in the interface. They are still sent in the request with the default value, so we show them.
- slots is not always about a file: enum-select means a choice from a list (voice, avatar).
- media_tokens: how the model recognizes files in the text. For Seedance reference-to-video the field looks like this: { "image": "@Image{n}", "video": "@Video{n}", "audio": "@Audio{n}", "numbering": "per-kind" }. The second file from image_urls is called @Image2 in the prompt. null for a single kind means there are no references of that kind in the text: the files go into the slot fields (first and last frame). If media_tokens is null as a whole, tokens are cut from the prompt.
- numbering inside media_tokens: whose scale the number {n} follows. With per-kind counting goes within each kind separately. With cross-kind there is one scale for videos and images, and videos come first: for the wan-v26-reference-to-video-flash variant with two videos and one image, the videos get Character1 and Character2, and the image gets Character3. This cannot be derived from the templates: both kinds have the same text. That is why the field comes separately and is present on every model with tokens.
- base_tokens is only the starting part of the price. For video, seconds beyond the included ones are counted separately.
How much it will cost
The estimate is calculated by the same code that later spends the tokens. So the number from /estimate matches the actual charge for the same parameters. There is no need to calculate on your side: for some models the price depends on duration, quality and text length at the same time.
curl -s -X POST https://pixyn.ai/api/v1/estimate \
-H "Authorization: Bearer $PIXYN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "fal-ai-kling-video-v3-pro-image-to-video",
"duration": 10,
"settings": { "resolution": "1080p" }
}'Avoid this: taking base_tokens from the catalog and showing it to the user as the total. For video it is the starting part, and on a ten-second clip the difference will be noticeable.Running and the result
Settings go as a flat settings object with keys from the catalog, media as a media object with slot keys. You do not need to figure out which provider expects parameters in which envelope: the server does that. The response comes right away with the pending status, and the generation runs in the background. Images are usually ready in seconds, video takes noticeably longer.
curl -s -X POST https://pixyn.ai/api/v1/generations \
-H "Authorization: Bearer $PIXYN_API_KEY" \
-H "Idempotency-Key: 8f14e45f-ea6a-4f2b-9c1d-0b6f2a1d77e0" \
-H "Content-Type: application/json" \
-d '{
"model": "fal-ai-kling-video-v3-pro-image-to-video",
"prompt": "a cat riding a skateboard along the embankment, evening light",
"duration": 10,
"settings": { "resolution": "1080p", "cfg_scale": 0.7 },
"media": { "start_image_url": "https://example.com/cat.jpg" }
}'
# the response has the id and the pending status; get the result like this:
curl -s https://pixyn.ai/api/v1/generations/GEN_ID \
-H "Authorization: Bearer $PIXYN_API_KEY"Idempotency-Key protects against double charging: with the same key you get the same generation back and a 200 response instead of 201. The key is remembered for a day. Set it on every run, since a network timeout does not mean the request did not arrive.
From a chat: MCP
MCP is how Claude, ChatGPT and VS Code connect external tools. Connect our server, and you can ask the assistant in plain words to generate an image or a video. It does this through your account and your balance and shows the result right in the conversation, so there is no need to fetch it from anywhere else.
Claude Code, with one command
claude mcp add --transport http pixyn https://pixyn.ai/api/mcp \
--header "Authorization: Bearer $PIXYN_API_KEY"Claude Desktop, Cursor and VS Code via config
You need exactly two values: the address https://pixyn.ai/api/mcp and the header Authorization: Bearer <your key>. The file format differs slightly between clients; here is the general form, put it where your client keeps its list of MCP servers.
{
"mcpServers": {
"pixyn": {
"type": "http",
"url": "https://pixyn.ai/api/mcp",
"headers": { "Authorization": "Bearer pxn_live_…" }
}
}
}The header with the key is not optional. Without it the server will be added and visible in the list, but the very first call will return 401: MCP has no login of its own, the key is the login.
What to tell the assistant
From there, use plain words: the model picks the tool and fills in parameters from the catalog by itself. There is no need to ask it to “call create_generation”.
“Show me which models can make video from an image”
“How much will 10 seconds in Kling v3 Pro at 1080p cost?”
“Animate this photo: let the camera slowly pull back. The start frame is at this link”
“Open the studio, I will pick the model myself”The assistant queues the generation, waits for it and shows a link to the result right in the conversation. If the model is paid and expensive, ask for an estimate first: it is a separate tool, and it does not charge anything.
- Tools: catalog, estimate, run, status, history, balance.
- A separate command opens a picker right in the chat with the same models, versions and settings as on the site.
- Running and getting the result are two separate calls. Video takes minutes, and the host will not let you wait for it inside one call.
- Permissions match the key’s: a key without run permission shows the catalog and estimates but does not spend tokens.
Delivery to your server
Optional. Useful when there is no chat at all and polling the status in a loop is inconvenient: a bot, a backend, a cron job. Subscribe an address in your account, and we will send a POST request when the generation finishes. Events: generation.completed, generation.failed, and for the Instagram section also instagram.lead (the keyword fired and the link went to direct messages) and instagram.message (an incoming message from a contact). The full reference with descriptions is returned by GET /api/user/webhooks/events.
X-Pixyn-Signature: sha256=<signature>
X-Pixyn-Timestamp: 1786000000
{
"event": "generation.completed",
"data": {
"id": "gen_…",
"status": "completed",
"output": ["https://gen.pixyn.ru/m/…"],
"error": null,
"tokens_spent": 70
}
}Verifying the signature is mandatory. Sooner or later the address ends up in logs or on a screenshot, and without verification anyone can send you a “result”. The signed string is “timestamp + dot + body”, and the subscription secret is the key. The timestamp is part of the signature on purpose: without it an intercepted request can be replayed forever.
import crypto from 'node:crypto'
function verify(rawBody, headers, secret) {
const ts = Number(headers['x-pixyn-timestamp'])
// older than five minutes: reject, otherwise an intercepted request can be replayed
if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(ts + '.' + rawBody)
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(headers['x-pixyn-signature'] ?? '')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}- The secret is shown once when the subscription is created, after that only a mask.
- Reply quickly with any 2xx and do the work asynchronously: we wait no longer than ten seconds.
- No reply? We retry up to five times with growing pauses. After ten failures in a row the subscription is disabled; the reason and code are visible in your account, where you can turn it back on.
- Only generations made with a key are delivered. What you make by hand on the site is not sent out.
- Subscriptions are managed only from your account; an API key cannot do this. Otherwise a stolen key could set up a channel for leaking results.
- The account buttons call three endpoints. All three work only after signing in to the account.
POST /api/user/webhooks/:id/testsends awebhook.testevent to the subscription address, signed the same way as regular events; the response has the fieldsdelivered,status_codeanderror.POST /api/user/webhooks/:id/enableturns a disabled subscription back on and resets the failed delivery counter.DELETE /api/user/webhooks/:iddeletes the subscription. If the subscription belongs to someone else or does not exist, the response is404 NOT_FOUND.
Limits
- No cancellation. Tokens are spent the moment the request is sent to the model, and a running generation cannot be stopped. Check the parameters with an estimate in advance.
- Refunds only for our failure. If it failed on our side, the tokens come back automatically. “I did not like the result” does not count as a reason for a refund.
- Files are accepted as links. There is no file body upload: media is passed as an absolute https link reachable by our server.
- History is visible only via the API. What you make on the site is not visible through the key.
- The number of concurrent generations is limited. Hit the ceiling? The endpoint responds 429 with the code CONCURRENCY_LIMIT_EXCEEDED. Wait for one of the running generations to finish and start the next one.
Errors
Each one has a machine code in the error field and an explanation in message. Errors in settings also return the list of allowed values, which shows what to fix.
| Code | HTTP | Meaning |
|---|---|---|
| BEARER_REQUIRED | 401 | No Authorization header, or it is not Bearer. |
| INVALID_TOKEN | 401 | The key is not recognized, has been revoked or has expired. |
| TOKEN_OWNER_NOT_FOUND | 401 | The key owner was not found. Issue the key again. |
| INSUFFICIENT_SCOPE | 403 | The key does not have permission for this endpoint. |
| ACCOUNT_BLOCKED | 403 | The key owner’s account is blocked. The reason is in the blockReason field. |
| INVALID_CATEGORY | 400 | Unknown category in the catalog filter. The response includes the list of allowed ones. |
| MODEL_REQUIRED | 400 | The request does not specify a model. |
| UNKNOWN_MODEL | 400 | There is no such model in the catalog. |
| UNKNOWN_SETTING | 400 | The model has no setting with this key. The response includes the list of available ones. |
| INVALID_SETTING_VALUE | 400 | The value is outside the allowed list or range. |
| INVALID_DURATION | 400 | The model does not accept this duration. |
| UNKNOWN_SLOT | 400 | The model has no such media slot. |
| INVALID_MEDIA_URL | 400 | The link is not an absolute https URL. |
| MEDIA_SLOT_LIMIT_EXCEEDED | 400 | The slot has more files than the model accepts. The slot limit is listed in the catalog. |
| REQUEST_REJECTED | 400 | The request failed the general check. The reason is in the code field, for example PROMPT_TOO_LONG_FOR_MODEL or PLAN_REQUIRED (the model needs a higher plan than yours). |
| IMAGE_INPUT_NOT_SUPPORTED | 400 | The model does not accept images, but the request has them. |
| IMAGE_INPUT_REQUIRED | 400 | The model edits an uploaded image, but the request has none. |
| REFERENCE_INPUT_REQUIRED | 400 | The model needs at least one reference: an image or a video. |
| IMAGE_TO_VIDEO_NEEDS_IMAGE | 400 | The model makes video from an image, but a video was sent. |
| INPUT_TYPE_MISMATCH | 400 | The model works with images and does not accept video. |
| VIDEO_MODEL_NEEDS_IMAGE | 400 | The model needs both a video and an image. |
| INPUT_LIMIT_EXCEEDED | 400 | More files than the model accepts, or the video is longer than its limit. |
| FILE_TOO_LARGE | 400 | The file is larger than the model limit. The limit is given in the message field. |
| SOURCE_FORMAT_UNSUPPORTED | 400 | The video format is not supported. MP4 or MOV is required. |
| SOURCE_CODEC_UNSUPPORTED | 400 | The model cannot read video in this codec. H.264 or HEVC is required. |
| SOURCE_RESOLUTION_TOO_HIGH | 400 | The video resolution is higher than the model accepts. |
| SOURCE_TOO_MANY_FRAMES | 400 | The video has more frames than the model accepts. |
| BEEBLE_MASK_REQUIRED | 400 | Replacing an object requires a mask. |
| BEEBLE_REFERENCE_REQUIRED | 400 | Replacing an object requires a scene reference. |
| BEEBLE_STYLE_INPUT_REQUIRED | 400 | A new background requires text or a scene reference. |
| BEEBLE_KEYFRAME_REQUIRED | 400 | The frame the mask is drawn on is not specified. |
| BEEBLE_KEYFRAME_OUT_OF_RANGE | 400 | The video has no mask frame with this number. |
| INPUT_MEDIA_PROBE_FAILED | 400 | Could not read the video or audio at the link. Retry the request. |
| INSUFFICIENT_TOKENS | 402 | Not enough tokens on the balance. |
| PLAN_REQUIRED | 403 | The model is not in the model list of your plan. |
| GENERATION_NOT_FOUND | 404 | There is no generation with this id among yours. |
| GENERATION_INFLIGHT | 409 | A request with this idempotency key is already running. |
| RATE_LIMIT_EXCEEDED | 429 | The per-minute request limit has been exceeded. |
| DAILY_MODEL_QUOTA_EXHAUSTED | 429 | The daily limit for this model on your plan is used up. |
| CONCURRENCY_LIMIT_EXCEEDED | 429 | The concurrent generation ceiling has been reached. The response has the limit and running fields. |
| CONCURRENCY_CHECK_UNAVAILABLE | 503 | Could not check the concurrent generation ceiling. Retry the request. |
When you do not need the API
A dozen generations a week by hand, and a key will not simplify anything. It is faster in the studio: you see the result, the history and hints for the settings. For repeating chains without code there is Workflow, and actions suit typical operations. A key makes sense when generation is built into your product or a regular process.
Issue a key
It is created in your account in a couple of seconds and works right away. Generation costs are listed on the pricing page; there is no separate fee for API access.
Profile → API