Developers / API
Make films from your own code
The same studio as the chat, over HTTP. Send a one-line ask or a site URL. Get a finished film in every format you need, and edit it with notes in plain words.
https://api.foxylab.ai/v1Step 1
Get a key
Make a key on the API keys page. The page shows it once.
Step 2
Start a film
Send
POST /filmswith aprompt, aurlor both.Step 3
Get the files
Wait for
film.ready, then callGET /films/{id}/renders.
- Format
- JSON in and JSON out, UTF-8
- Ids
- film_, evt_ and req_ come before the id
- Times
- UTC, in ISO 8601
- Version
- /v1 in the path. New fields can come at any time
Authentication
Send your API key in the Authorization header of every request, after the word Bearer. Make keys on the API keys page.
A key shows only once. Keep it on your server, in a secret manager or an environment variable. Do not put a key in a browser, an app or a repository.
Scopes
films:read- Read films, stages, versions and renders.
films:write- Make films, give notes and restore versions. Uses the usage limit.
/v1/films /
Host: api.foxylab.ai
Authorization: Bearer Usage limits
Every call draws on the same usage limit as the studio: the limit of your plan. A film made over the API counts the same as a film made in the chat.
There is no price for each video and no price for each token. Your team, your agents and your code all use one limit.
When what is left cannot pay for the work, calls that start work return 402 with usage_limit_reached. The message says what the work needs and what is left; add_usage_url is where to add extra usage, and resets_at says when the limit comes back. See the limit of each plan on Pricing.
{
"error": {
"type": ,
"message": ,
"resets_at": ,
"needed_usd": ,
"left_usd": ,
"add_usage_url": ,
"request_id":
}
}/films
Create a film
Starts a new film from a one-line ask, a site or both. The call returns at once with the film id. The studio then runs the seven stages on its own.
The film also opens in your studio, so a person on your team can keep editing it in the chat. Send an Idempotency-Key header to make a retry safe. The same key within 24 hours returns the same film.
Body
promptstring- The ask in plain words, for example a one-line brief. Send
prompt,urlor both. urlstring- A site for the studio to read. It takes the product, the colours, the fonts and the tone from it.
formatsstring[]- The sizes to make:
9:16,4:5,1:1,16:9. The default is9:16. languagestring- The film's language, for example
Ukrainian. The default isEnglish. voicestring- A voice name such as
sulafat, a few words such aswarm, slow, ornonefor music only. Without it, the studio picks a voice for the brand. voice_modestring- Where the voice speaks.
auto, the default: the studio decides whether a narrator helps this film, and where.parts: it stops where the picture and the music say more.full: every shot is spoken.none: no narrator. musicstringauto, the default: the studio decides whether the film needs a music bed, and picks it from free stock.on: always a bed.off: no music. A CC BY track comes with a credit line for the post; renders return it.music_levelstring- How far the music sits under the voice:
low,normal(the default) orhigh. The voice always stays at least 15 dB above it. brand_kitstring- The id of a brand kit from your studio, for example
kit_mossly. It wins over what the studio reads fromurl.
POST https://api.foxylab.ai/v1/films \
\
\
'{
"prompt": ,
"url": ,
"formats": [, ],
"voice": ,
"voice_mode":
}'{
"id": ,
"object": ,
"status": ,
"stage": ,
"version": ,
"formats": [, ],
"studio_url": ,
"created_at":
}/films/
Get a film
Returns the film and the status of each stage: waiting, working, done or failed. The film status is working until the render is done, then ready.
Poll every 10 seconds or more, or use webhooks and do not poll at all.
- Briefdone
- Branddone
- Ideadone
- Voicedone
- Shotsworking
- Reviewwaiting
- Renderwaiting
Path
idstring- The film id, for example
film_8Kq2xV.
https://api.foxylab.ai/v1/films/film_8Kq2xV \
{
"id": ,
"object": ,
"title": ,
"status": ,
"version": ,
"stages": [
{ "id": , "status": },
{ "id": , "status": },
{ "id": , "status": },
{ "id": , "status": },
{ "id": , "status": , "progress": },
{ "id": , "status": },
{ "id": , "status": }
],
"eta_seconds":
}/films
List films
Returns the films of your team, the newest first. Films made in the studio chat are in the list too.
Query
statusstring- Only films with this status:
working,readyorfailed. limitinteger- How many films to return, from 1 to 100. The default is 20.
cursorstring- The
next_cursorfrom the last page, to get the next page.
\
{
"object": ,
"data": [
{
"id": ,
"title": ,
"status": ,
"version": ,
"updated_at":
},
{
"id": ,
"title": ,
"status": ,
"version": ,
"updated_at":
}
],
"next_cursor":
}/films//notes
Add a note
Gives the film a note in plain words, the same as a note in the chat. The studio runs again only the stages the note touches and makes a new version.
The old version stays. You can bring it back with restore.
Path
idstring- The film id, for example
film_8Kq2xV.
Body
textstring- The note in plain words, as you write it in the studio chat.
refsobject[]- Points the note at a part of the film:
{ "shot": "s3" },{ "time": 14.7 }in seconds, or{ "format": "16:9" }.
POST https://api.foxylab.ai/v1/films/film_8Kq2xV/notes \
\
\
'{
"text": ,
"refs": [
{ "shot": },
{ "time": }
]
}'{
"film_id": ,
"version": ,
"based_on": ,
"status": ,
"stages": [, , , ]
}/films//versions
List versions
Returns every version of the film with the note that made it. Version 1 has no note.
Path
idstring- The film id, for example
film_8Kq2xV.
https://api.foxylab.ai/v1/films/film_8Kq2xV/versions \
{
"object": ,
"current": ,
"data": [
{
"version": ,
"note": ,
"status": ,
"created_at":
},
{
"version": ,
"note": ,
"status": ,
"created_at":
}
]
}/films//restore
Restore a version
Brings back an earlier version. The film gets a new version with the old content, so nothing is lost. The files are already rendered, so the film is ready at once.
Path
idstring- The film id, for example
film_8Kq2xV.
Body
versioninteger- The version to bring back. The film gets a new version with the same content, so you keep the history.
POST https://api.foxylab.ai/v1/films/film_8Kq2xV/restore \
\
\
'{
"version":
}'{
"film_id": ,
"version": ,
"restored_from": ,
"status":
}/films//renders
Get renders
Returns the files for each format and file type: MP4 for ads and social, MOV in ProRes for editors, WebM for your site and GIF for chats.
The links work for 24 hours. Call again for fresh links.
Path
idstring- The film id, for example
film_8Kq2xV.
Query
versioninteger- A version of the film. The default is the current version.
formatstring- Only this size, for example
16:9. typestring- Only this file type:
mp4,mov,webmorgif.
https://api.foxylab.ai/v1/films/film_8Kq2xV/renders \
{
"film_id": ,
"version": ,
"renders": [
{
"format": ,
"width": ,
"height": ,
"files": [
{
"type": ,
"bytes": ,
"url":
},
{
"type": ,
"bytes": ,
"url":
}
]
},
{
"format": ,
"width": ,
"height": ,
"files": [
{
"type": ,
"bytes": ,
"url":
}
]
}
],
"expires_at":
}Webhooks
Webhooks tell your server about a film, so you do not poll. Add the endpoint URL on the API keys page. The page shows its signing secret, which starts with whsec_.
Events
film.stage- A stage is done or has failed.
film.ready- The film is reviewed and rendered in its first format.
render.ready- All files for one format are ready to download.
Check the signature
- 1Read the raw body before you parse it.
- 2Split the
FoxyLab-Signatureheader intotandv1. - 3Make an HMAC SHA-256 of
t.bodywith your signing secret. - 4Compare it with
v1in constant time. Refuse events older than 5 minutes. - 5Answer with a 2xx status within 10 seconds. Do slow work after the answer.
A delivery without a 2xx answer comes again, with a longer wait each time, for 24 hours. An event can come more than once, so use its id to skip repeats.
/hooks/foxylab /
Content-Type: application/json
FoxyLab-Signature: t=,v1=bfc7dd7386a075c7d6c0607c6d11a6a81dfbe4537a99606830d459cc961aee20{
"id": ,
"type": ,
"created_at": ,
"data": {
"film_id": ,
"version": ,
"title": ,
"seconds":
}
} crypto ;
// rawBody: the request body as a string, before JSON.parse
verify(rawBody, header, secret) {
parts = Object.fromEntries(
header.split().map((p) => p.split()),
);
expected = crypto
.createHmac(, secret)
.update()
.digest();
age = Date.now() / - Number(parts.t);
(
age < &&
expected.length === parts.v1?.length &&
crypto.timingSafeEqual(
Buffer.(expected),
Buffer.(parts.v1),
)
);
}Errors
Errors use HTTP status codes and one JSON shape. Send the request_id to support when you ask for help.
- 400
invalid_requestA field is missing or wrong.paramnames it. - 401
unauthorizedThe key is missing, wrong or revoked. - 403
forbiddenThe key does not have the scope for this call. - 404
not_foundThere is no film with this id on your team. - 409
film_not_readyThe film is still working. Wait forfilm.ready. - 422
url_unreadableThe studio could not open the site. Send apromptor abrand_kit. - 429
rate_limitedToo many requests. Wait for theRetry-Afterseconds. - 402
usage_limit_reachedThe week's usage cannot pay for this. The message says what it needs and what is left;add_usage_urlis where to add extra usage,resets_atwhen the week resets (every Friday at 00:00 UTC). - 500
server_errorOur fault. Retry with the sameIdempotency-Key.
{
"error": {
"type": ,
"message": ,
"param": ,
"request_id":
}
}Rate limits
Rate limits keep the service fast for everyone. They are not the usage limit of your plan.
- Requests for each key
- 120 a minute
- New films for each team
- 10 a minute
- Films working at the same time
- 5 for each team
Every response has the RateLimit-* headers. After a 429, wait for the seconds in Retry-After, then try again.
/ Too Many Requests
RateLimit-Limit:
RateLimit-Remaining:
RateLimit-Reset:
Retry-After: