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(anyW:Hfrom 9:21 to 21:9) withmegapixels, orwidthandheight. 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. modelisstandard(default) orpro.progives finer detail and the most accurate lettering at several times the price; it takes no@labelreferences (a prompt that mentions one is refused withreferences_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.textis required for text regions and is written exactly. See Prompting images.negativePromptis optional and roughly doubles render time and price with the standard model.@labelmentions in the prompt attach references of the same project (the image library has its own).- In a sequence the result is a
startasset in the first segment, selected automatically if nothing is selected yet; in the library it is animageasset.
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 andtarget: "image"edits only) makes the result a game sprite:{"width", "height", "transparent", "mode", "fit", "padding", "palette", "key", "outline", "anchor", "mirror", "tile", "baseColor"}, all optional exceptwidthfor a new sprite.paletteis a colour count or a list of#rrggbbcolours.spriteNamenames 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, andanimation:{"kind": "action", "frames": 2-8}on atarget: "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"(noprojectIdorsegmentId) edits any image you own and saves the result as a libraryimage, 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 anendcandidate at the project's frame size.target: "start"(first segment only) edits one of its start-frame candidates and produces anotherstartcandidate.@frameis the image being edited;@labelmentions 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 |
framescounts 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(defaulttrue) keeps the generated audio track.smooth(defaulttrue) delivers 48 fps instead of 24, free.- A finished video job's
outputAssetIdsare[clip, lastFrame], plus the first frame intextmode. The clip'smeta.lastFrameAssetIdpoints 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, withjob(every status and progress change),credits(new balance) andproject(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.