Keytween

Reference

REST API reference

Base URL: https://keytween.com/api/v1. Requests and responses are JSON unless noted. The API is versioned in the path; fields may be added to responses without a version change.

The REST API uses the web app's own names: a project is a sequence and a segment is a step. Field names are camelCase.

Authentication #

curl -s https://keytween.com/api/v1/projects -H "Authorization: Bearer $KEYTWEEN_API_KEY"

Create keys on the Account page (they unlock after your first credit purchase). Keys start with sk_test_. A key acts as its owner and spends the owner's credits; it cannot manage keys, change the password, delete the account or use administration endpoints.

Errors #

Every error has the same shape and a matching HTTP status:

{ "error": { "code": "insufficient_credits", "message": "Not enough credits for this generation" } }

400 invalid input, 401 unauthenticated, 402 insufficient credits, 403 forbidden, 404 not found, 409 conflict with the current state, 429 rate or concurrency limit. The codes are listed in Jobs, waiting and limits.

Objects #

Project: id, kind (sequence, or images for the image library), title, width, height (null until a start frame is selected), segmentCount, totalFrames, createdAt, updatedAt. The detail form adds segments, assets and exports.

Segment: id, idx (0 for the first), startAssetId, endAssetId, clipAssetId (the selected take), videoPrompt, frames, acceptedAt.

Asset: id, projectId, segmentId, kind (image or video), role (image for a standalone library image, start, end, clip, last_frame, reference, export), width, height, frames, jobId, url, thumbUrl, meta, createdAt. meta carries prompt, negativePrompt, seed, origin, and for takes mode, startAssetId, endAssetId, lastFrameAssetId, fps, sound; for references label. url and thumbUrl are paths on this host that need the same Authorization header; add ?download=1 to url for a file download. Range requests are supported.

Job: id, type (t2i, edit, video), status (queued, running, succeeded, failed, cancelled), progress (0 to 1), phase, queuePosition, etaSeconds, cost, error, outputAssetIds, params, projectId, segmentId, createdAt, startedAt, finishedAt.

Generating #

POST /jobs starts a generation and returns 202 with the job. POST /jobs/quote takes the same body and returns {cost, width, height, referenceAssetIds, compiledPrompt, seconds} without charging or creating anything; seconds is the estimated render time. When the shared queue is full or rendering is offline, POST /jobs returns 503 (queue_full or renderer_offline) and charges nothing.

Standalone images live in the image library, a special project that GET /images returns (with its references; it is created on first use). Leave projectId out of a text-to-image request to make a library image, or pass a sequence's projectId to make a start-frame candidate there.

Text to image #

{
  "type": "t2i",
  "projectId": "…",
  "prompt": "A wide cinematic photograph of …",
  "aspect": "16:9",
  "megapixels": 0.5,
  "seed": 123
}
  • Size: aspect (any W:H from 9:21 to 21:9) with megapixels, or width and height. Library images: 0.3 to 4 megapixels, 1:1 at 1 megapixel by default. Sequence frames: 0.3 to 1 megapixels, defaulting to the project's frame size, or 16:9 at 0.5 megapixels if it has none. See Sizes and durations.
  • model is standard (default) or pro. pro gives finer detail and the most accurate lettering at several times the price; it takes no @label references (a prompt that mentions one is refused with references_unsupported) and its negative prompt costs nothing extra. See Pricing.
  • regions (pro only, up to 12): boxes that pin parts of the picture, each {"kind": "object" | "character" | "text", "caption", "text", "x", "y", "width", "height"} with coordinates as fractions of the canvas from the top left. text is required for text regions and is written exactly. See Prompting images.
  • negativePrompt is optional and roughly doubles render time and price with the standard model.
  • @label mentions in the prompt attach references of the same project (the image library has its own).
  • In a sequence the result is a start asset in the first segment, selected automatically if nothing is selected yet; in the library it is an image asset.

Edit #

{
  "type": "edit",
  "projectId": "…",
  "segmentId": "…",
  "target": "end",
  "sourceAssetId": "…",
  "prompt": "Give the gnome in @frame the exact same hat shown in @hat. Keep everything else unchanged."
}
  • sprite (text-to-image and target: "image" edits only) makes the result a game sprite: {"width", "height", "transparent", "mode", "fit", "padding", "palette", "key", "outline", "anchor", "mirror", "tile", "baseColor"}, all optional except width for a new sprite. palette is a colour count or a list of #rrggbb colours. spriteName names the result. See Game sprites and icons.
  • Sprite jobs also take variants (1-4 candidates), engine ("standard" or "premium", text-to-image only), styleAssetIds (up to 3 images whose look to match), styleKitId, and animation: {"kind": "action", "frames": 2-8} on a target: "image" edit animates the source sprite (the prompt describes the action); {"kind": "walk"} makes a 4-direction walk sheet from the prompt (text-to-image) or from the source (edit). Sprites take no negative prompt.
  • target: "image" (no projectId or segmentId) edits any image you own and saves the result as a library image, at the source's shape and at most 2 megapixels. References come from the image library.
  • target: "end" edits the segment's selected start frame (the default source) or one of its end-frame candidates, and produces an end candidate at the project's frame size.
  • target: "start" (first segment only) edits one of its start-frame candidates and produces another start candidate.
  • @frame is the image being edited; @label mentions attach references, 4 at most.

Video #

{
  "type": "video",
  "projectId": "…",
  "segmentId": "…",
  "mode": "first_last",
  "prompt": "Daylight fades over the street as …",
  "frames": 78,
  "sound": true,
  "smooth": true
}
mode Needs Notes
first_last (default) selected start and end frames lands exactly on the end frame
image a selected start frame open ending
text nothing first segment only; accepts aspect, megapixels, width, height; the clip's first frame is saved as a start asset and selected if the segment has no start frame
  • frames counts 1/18 of a second: 60 to 180 in steps of 6 (3.33 to 10 s; at most 120 above 0.6 megapixels). Other values are snapped.
  • sound (default true) keeps the generated audio track. smooth (default true) delivers 48 fps instead of 24, free.
  • A finished video job's outputAssetIds are [clip, lastFrame], plus the first frame in text mode. The clip's meta.lastFrameAssetId points at its last frame.

Following a job #

  • GET /jobs/:id: the job now. Poll every few seconds.
  • GET /jobs?projectId=&active=true&limit=: your jobs, newest first.
  • POST /jobs/:id/cancel: cancel and refund.
  • GET /events: one long-lived Server-Sent Events stream for the account, with job (every status and progress change), credits (new balance) and project (something in a project changed) events. This is the efficient way to follow many jobs.

Projects and segments #

Method Path
GET /projects list
POST /projects create: {"title", "fromAssetId"}. fromAssetId (optional) copies any of your images in as the selected start frame, scaled to at most 1 MP. Returns the detail form with one segment
GET /images the image library project with its references
POST /images/sprites {"sourceAssetId", "sprite", "name"}: free re-export of a sprite or flat-background image as a sprite
GET /style-kits Saved style kits
POST /style-kits {"name", "referenceAssetIds", "styleText", "outline", "palette"}: save a style kit
PATCH /style-kits/:id Change any of those fields
DELETE /style-kits/:id Delete a style kit (its sprites stay)
GET /projects/:id detail: segments, assets, exports
PATCH /projects/:id rename: {"title"}
DELETE /projects/:id delete the project and everything in it
PATCH /projects/:id/segments/:sid select candidates and set defaults: any of startAssetId, endAssetId, clipAssetId (null clears the last two), videoPrompt, frames
POST /projects/:id/segments/:sid/accept accept the selected take; creates the next segment starting on its last frame
POST /projects/:id/segments/:sid/reopen un-accept; deletes every later segment
POST /projects/:id/export stitch the selected takes into one MP4; returns the export asset. Free

Images, references and files #

Method Path
POST /projects/:id/uploads multipart: field role (start or reference in a sequence; image or reference in the image library), optional label, then file (PNG, JPEG or WebP, up to 25 MB). Fields must come before the file
POST /projects/:id/references {"sourceAssetId", "label"}: copy any of your images in as a reference
PATCH /projects/:id/assets/:assetId {"label"}: rename a reference
POST /projects/:id/assets/:assetId/crop {"x", "y", "w", "h"} as fractions 0 to 1: crop a reference
GET /references your references across all projects
DELETE /projects/:id/assets/:assetId delete an unselected candidate, take or export
GET /assets/:id/file the file; ?download=1 for an attachment
GET /assets/:id/thumb a small WebP preview
curl -s https://keytween.com/api/v1/projects/$P/uploads \
  -H "Authorization: Bearer $KEYTWEEN_API_KEY" \
  -F role=reference -F label=hat -F [email protected]

Library #

Method Path
GET /library?group=&projectId=&cursor=&limit= everything on the account, newest first. group is images, videos, references or exports. Returns {items, nextCursor, totals}; each item is an asset plus projectTitle, bytes, selected, locked and via
POST /library/delete {"assetIds": [...]}: delete items anywhere on the account. Returns {deleted, deletedProjects, failed}

Unlike the per-project delete, /library/delete also removes an item that is selected in an open segment (it is deselected first), and removes a project that becomes empty. Items in accepted segments fail with asset_locked. See Your data and deleting it.

Account #

Method Path
GET /auth/me the account behind the key: user.credits, user.permissions, user.trusted
GET /me/ledger recent credit history
GET /meta gpu.online, gpu.accepting (false while the queue is full), gpu.queueRemaining and gpu.waitSeconds
GET /billing purchase settings and credit packs

Credits are bought on the website. See Credits and pricing.

A complete example #

One shot from text to finished clip:

API=https://keytween.com/api/v1
H="Authorization: Bearer $KEYTWEEN_API_KEY"
J="Content-Type: application/json"

wait_job() {
  while :; do
    S=$(curl -s -H "$H" $API/jobs/$1 | jq -r .status)
    case $S in succeeded) return 0;; failed|cancelled) return 1;; esac
    sleep 3
  done
}

P=$(curl -s -H "$H" -H "$J" -d '{"title":"API reel"}' $API/projects | jq -r .id)
S=$(curl -s -H "$H" $API/projects/$P | jq -r '.segments[0].id')

JOB=$(curl -s -H "$H" -H "$J" $API/jobs -d "{\"type\":\"t2i\",\"projectId\":\"$P\",\"aspect\":\"16:9\",
  \"prompt\":\"A wide photograph of a closed flower bud on a windowsill at dawn\"}" | jq -r .id)
wait_job $JOB

JOB=$(curl -s -H "$H" -H "$J" $API/jobs -d "{\"type\":\"edit\",\"projectId\":\"$P\",\"segmentId\":\"$S\",\"target\":\"end\",
  \"prompt\":\"The flower in @frame is now fully open, petals spread wide. Keep everything else unchanged.\"}" | jq -r .id)
wait_job $JOB

JOB=$(curl -s -H "$H" -H "$J" $API/jobs -d "{\"type\":\"video\",\"projectId\":\"$P\",\"segmentId\":\"$S\",\"frames\":90,
  \"prompt\":\"The bud slowly opens into full bloom as morning light brightens. The camera remains static. Birdsong outside, a faint breeze.\"}" | jq -r .id)
wait_job $JOB

CLIP=$(curl -s -H "$H" $API/jobs/$JOB | jq -r '.outputAssetIds[0]')
curl -s -H "$H" -o bloom.mp4 "$API/assets/$CLIP/file"

To continue: POST /projects/$P/segments/$S/accept, read the new segment's id from the response, make its end frame with an edit job on that segment, render it, and finally POST /projects/$P/export.

Limits #

600 requests per minute per address, 30 job submissions per minute, and a per-account limit on generations running at once. See Jobs, waiting and limits.

All docs