24%

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

  1. Issue a key and put it in an environment variable.
  2. Fetch the catalog: model, setting and slot keys come from there.
  3. Ask for an estimate: how much your exact request will cost.
  4. 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.

MethodPathScopePurpose
GET/api/v1/menot neededWhose key it is, its scopes and limits.
GET/api/v1/modelsmodels:readCatalog: groups, versions, settings, slots, price.
POST/api/v1/estimatemodels:readHow many tokens such a request will cost.
POST/api/v1/generationsgenerations:createRun a generation.
GET/api/v1/generations/:idgenerations:readStatus and result of one generation.
GET/api/v1/generationsgenerations:readList 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/test sends a webhook.test event to the subscription address, signed the same way as regular events; the response has the fields delivered, status_code and error. POST /api/user/webhooks/:id/enable turns a disabled subscription back on and resets the failed delivery counter. DELETE /api/user/webhooks/:id deletes the subscription. If the subscription belongs to someone else or does not exist, the response is 404 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.

CodeHTTPMeaning
BEARER_REQUIRED401No Authorization header, or it is not Bearer.
INVALID_TOKEN401The key is not recognized, has been revoked or has expired.
TOKEN_OWNER_NOT_FOUND401The key owner was not found. Issue the key again.
INSUFFICIENT_SCOPE403The key does not have permission for this endpoint.
ACCOUNT_BLOCKED403The key owner’s account is blocked. The reason is in the blockReason field.
INVALID_CATEGORY400Unknown category in the catalog filter. The response includes the list of allowed ones.
MODEL_REQUIRED400The request does not specify a model.
UNKNOWN_MODEL400There is no such model in the catalog.
UNKNOWN_SETTING400The model has no setting with this key. The response includes the list of available ones.
INVALID_SETTING_VALUE400The value is outside the allowed list or range.
INVALID_DURATION400The model does not accept this duration.
UNKNOWN_SLOT400The model has no such media slot.
INVALID_MEDIA_URL400The link is not an absolute https URL.
MEDIA_SLOT_LIMIT_EXCEEDED400The slot has more files than the model accepts. The slot limit is listed in the catalog.
REQUEST_REJECTED400The 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_SUPPORTED400The model does not accept images, but the request has them.
IMAGE_INPUT_REQUIRED400The model edits an uploaded image, but the request has none.
REFERENCE_INPUT_REQUIRED400The model needs at least one reference: an image or a video.
IMAGE_TO_VIDEO_NEEDS_IMAGE400The model makes video from an image, but a video was sent.
INPUT_TYPE_MISMATCH400The model works with images and does not accept video.
VIDEO_MODEL_NEEDS_IMAGE400The model needs both a video and an image.
INPUT_LIMIT_EXCEEDED400More files than the model accepts, or the video is longer than its limit.
FILE_TOO_LARGE400The file is larger than the model limit. The limit is given in the message field.
SOURCE_FORMAT_UNSUPPORTED400The video format is not supported. MP4 or MOV is required.
SOURCE_CODEC_UNSUPPORTED400The model cannot read video in this codec. H.264 or HEVC is required.
SOURCE_RESOLUTION_TOO_HIGH400The video resolution is higher than the model accepts.
SOURCE_TOO_MANY_FRAMES400The video has more frames than the model accepts.
BEEBLE_MASK_REQUIRED400Replacing an object requires a mask.
BEEBLE_REFERENCE_REQUIRED400Replacing an object requires a scene reference.
BEEBLE_STYLE_INPUT_REQUIRED400A new background requires text or a scene reference.
BEEBLE_KEYFRAME_REQUIRED400The frame the mask is drawn on is not specified.
BEEBLE_KEYFRAME_OUT_OF_RANGE400The video has no mask frame with this number.
INPUT_MEDIA_PROBE_FAILED400Could not read the video or audio at the link. Retry the request.
INSUFFICIENT_TOKENS402Not enough tokens on the balance.
PLAN_REQUIRED403The model is not in the model list of your plan.
GENERATION_NOT_FOUND404There is no generation with this id among yours.
GENERATION_INFLIGHT409A request with this idempotency key is already running.
RATE_LIMIT_EXCEEDED429The per-minute request limit has been exceeded.
DAILY_MODEL_QUOTA_EXHAUSTED429The daily limit for this model on your plan is used up.
CONCURRENCY_LIMIT_EXCEEDED429The concurrent generation ceiling has been reached. The response has the limit and running fields.
CONCURRENCY_CHECK_UNAVAILABLE503Could 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