Make videos, songs, pictures, presentations and voices from your own tools
Everything the app does, from a script or a server: describe a scene, add pictures, get a short video with sound; say what a song is about, get it written and sung; keep a voice and have it say your texts. Everything made with a key counts on the key owner's week, exactly like in the app.
Your key
Create one on the app's Account screen (“API access”). It is shown once. Send it as a bearer token on every call:
Authorization: Bearer ip_xxxxxxxxxxxxxxxxxxxxxxxx
A key reaches the studio's trades only (https://api.infinitepeople.ai/v1/video/… and /v1/song/… today, the other trades as they come); it cannot change your subscription or make other keys. Revoke it from the same screen if it leaks — or let the key burn itself: DELETE /v1/studio/key with that key revokes it at once. The app itself runs on these very routes: whatever it can do, a key can do, with the same account, the same week and the same plan.
Make a video
curl -X POST https://api.infinitepeople.ai/v1/video/generate \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"model": "minimax-h3-ref2va",
"prompt": "Mia hails a taxi in the rain, shouting \"Taxi!\"",
"reference_urls": ["https://example.com/mia.jpg"],
"duration_s": 5,
"enhance": "low",
"review": false
}'
The answer is the creation: {"id": "…", "status": "enhancing", "ready": false, "keep_polling": true, "poll": "https://api.infinitepeople.ai/v1/video/jobs/…", "next": "Fetch 'poll' right now…", …}. Do not wait or sleep: fetch poll right away — it holds the connection up to 25 seconds and answers as soon as something changes (?wait=0 answers at once; GET or POST alike):
curl https://api.infinitepeople.ai/v1/video/jobs/$JOB_ID -H "Authorization: Bearer $KEY"
One stop signal: ready: true — result.url is the mp4 (kept until expires_at — the account's retention, a year by default; a video kept in a project stays for good), result.poster_url its first frame, result.last_poster_url its last one — or keep_polling: false with a code (generation_failed, cancelled). next always says what to do; page_for_your_human is the creation in the app. The job's own fields (status, request, result_url…) stay as they were. A finished video also carries creation_id, retention, expires_at, and access — owner, edit or view (a video shared with you: POST /v1/creations/{creation_id}/shares by its owner, the link accepted at /v1/shares/{token}/accept) — with owner {id, name} when it is not yours; view opens the record and the files, edit also the gestures (upscale, sound, a new take), and what you make from it comes back to its owner shared for editing; a view-only share on a gesture answers 403 share_view_only; deleting is the owner's alone.
Two models
| model | pictures | use it for |
|---|---|---|
minimax-h3-ref2va | reference_urls (up to 4) | people, places and styles the video draws on; cite them in the prompt as <Picture 1>, <Picture 2>… A voice_url (mp3, wav, m4a) gives the voice, cited as <Audio 1>. |
minimax-h3-fl2va | first_frame_url, optional last_frame_url | the video starts on the first picture and lands on the second; no picture at all works too (text only). Pictures are brought to the clip's shape (see Shape). |
Fields
| field | values | |
|---|---|---|
prompt | text | What happens: who, where, what moves. One sentence is enough. Written in any language; a line of dialogue in quotes is spoken as is. |
duration_s | 5, 10, 15 | Up to 15 s, as the plan allows. |
enhance | off, low, medium, high | The direction help: off sends your prompt as is; low (default) rewrites it for the model; medium and high also paint the first image from your pictures or your text (high: twice as detailed). What your plan allows: GET /v1/video/me → limits.enhance_levels. |
definition | low, high | 864×480 and 1344×768 at 16:9, the same pixel count at another shape (15 s at either). 768p is the ceiling of the public weights; a take you like is brought higher by the engine afterwards (below). |
aspect | 16:9, 9:16, 1:1, 4:3, 3:4, 21:9, auto | The clip's shape; auto follows the first frame given, 16:9 without one. |
fit | auto, crop, bars, extend | A start or end picture of another shape than the clip's: auto (default) crops it when close, sets it on black bars when far; crop and bars force one; extend has gpt-image continue the scene past its edges (a cloud step, a few cents on the week; with a subscription). To keep a picture's own shape: aspect auto. |
confidentiality | low, medium, high | Where the work may run (default: the account's setting, low). low: the best tool wherever it is. medium: our machines and contracted providers that never train on your content, results stored in Europe. high: our own machines only — not open yet; GET /v1/studio/me → confidentiality.levels says what each level allows today. |
quality | draft, standard, fine | draft: the sparse attention from 60 % of the schedule (and the 4-step schedule at 768p) — about 40 % less time (a 5 s clip: 18 s in 480p, 36 s in 768p), a hair less micro-detail on skin and hair, the same take to the eye; the rung to search on. standard (default): what the studio delivers (31 s, 59 s). fine: the full 20-step schedule, about twice the week of standard, for a take you already like — not for searching. An upscale renders with standard. |
audio | true / false | The soundtrack, on by default. |
review | true / false | With a painted first image, the app shows it for 10 s before rendering. From a script, send false: the job goes straight to rendering. |
seed | integer | Fix it to reproduce a take. |
negative_prompt | text | What to steer away from. |
Your own pictures
A picture must be reachable by URL. If it is not hosted anywhere, send it first (JPEG, PNG or WebP, 8 MB max); the URL lives 7 days:
curl -X POST https://api.infinitepeople.ai/v1/video/inputs \
-H "Authorization: Bearer $KEY" -F "[email protected]"
# → {"url": "https://media.infinitepeople.ai/ephemeral/inputs/…jpg"}
Pictures kept in a project (GET /v1/projects) have permanent URLs you can reuse directly.
Shape
aspect: 16:9 (the default), 9:16, 1:1, 4:3, 3:4, 21:9, or auto: the shape closest to the first frame you give (a 4:3 picture gives a 4:3 video; a 3:2 one the 4:3 shape with a centred crop of a few percent, at most 15 % of one side — never a stretch). A definition keeps its pixel count whatever the shape (low 864×480 or 480×864 or 640×640 or 544×736…, high 1344×768 or 768×1344 or 1024×1024), every side a multiple of 32. 24 frames per second, 5 to 15 seconds. A start or end frame of another shape than the one asked follows fit: auto crops it around its centre when it is close (within ×1.6) and sets it on black bars when it is the other orientation, crop and bars force one or the other, extend has gpt-image continue the scene past its edges at any direction level (off included; the estimate's cost_usd counts one call per distinct picture; when gpt-image declines a picture the clip goes on with the auto rule and the job's request says so under enhanced.fallback). With a start or end picture, the direction help paints nothing: medium and high are a rewrite like low. POST /v1/video/estimate with the same body answers the geometry, seconds (the render), director_seconds (what the direction help adds before it, review included) and total_seconds, without launching anything.
What a video would take, before launching it
curl -X POST https://api.infinitepeople.ai/v1/video/estimate \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"model": "minimax-h3-fl2va", "prompt": "…", "definition": "low", "quality": "draft", "duration_s": 5}'
Same body as the launch, nothing is queued. The answer says the exact pixels and frames the request resolves to, seconds (the render), director_seconds (what the direction help adds before it), total_seconds, the share of your week it would take, and online — whether a machine that serves this model is up. Useful to compare the tiers before spending: the same request in low/draft and in high/standard answers 18 s and 65 s of render.
The first image under review
With enhance: medium or high and review: true, the job stops in status review for ten seconds with its painted first image (request.enhanced.painted_urls[0]), then renders on its own. Act on it meanwhile:
curl -X POST https://api.infinitepeople.ai/v1/video/jobs/$JOB_ID/review \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"action": "approve"}' # or "pause", "resume", or "regen" with "panels": [1] to repaint it
# "regen" takes a "note" (what is wrong, a sentence): the painter follows it, and every earlier note still holds
Start again from a video
A finished video gives pictures to start from: poster_url (its first frame), last_poster_url (its last), or any moment:
curl -X POST https://api.infinitepeople.ai/v1/video/jobs/$JOB_ID/frame \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"t": 2.5}'
# → {"url": "https://media.infinitepeople.ai/ephemeral/inputs/…jpg", "t": 2.5}
Use that URL as first_frame_url (a sequel), last_frame_url, or in reference_urls; add "edited_from": "$JOB_ID" to keep the lineage, and the same seed for the same take. A cancelled job: DELETE /v1/video/jobs/$JOB_ID (free before the render; counted once a node has it, the answer says charged). The same call puts a finished video in the bin: 48 h to take it back (POST /v1/creations/{creation_id}/restore, the id is in the answer), then its files are purged.
Bring a take to a higher definition
A finished video is rendered again by the engine at a higher definition, the video itself driving the render, so you get the same take (a new render at another definition would be another take: the seed only reproduces a video at the same geometry). Search in draft, then bring the take you keep up:
curl -X POST https://api.infinitepeople.ai/v1/video/jobs/$JOB_ID/upscale \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"definition": "high"}'
# → a new creation, "edited_from": "$JOB_ID", "request.upscale_from": {…}; poll it like any other
The engine follows the source frame by frame, but it paints every pixel again: small alterations can happen (a strand of hair, the skin's grain, a micro-expression). It comes out with two soundtracks on the same picture: the source's, kept bit for bit (what result.url plays), and the engine's own — result.sounds lists both, POST /v1/video/jobs/$JOB_ID/sound {"id": "new"} makes result.url play the other (what a project keeps, what the bench places). It counts on the week like a render at that definition times 1.45 (the video guides every step); the plan must allow the definition; 422 not_higher when the definition is not above the video's, 409 not_done on a video not finished. The source must still be there (its own expires_at).
1080p is another gesture on the same route: {"definition": "full_hd"} brings the finished video itself to 1080 pixels on its short side (1944x1080 from a 16:9 480p clip, its shape kept), frame by frame, with the detail the upscaler reconstructs (skin, hair, teeth) — the take, its motion and its soundtrack stay exactly what they were, one file, no second soundtrack. Two engines, chosen with quality. fast is the default and what to use: the picture is reconstructed — skin, hair, teeth — for about 20 s plus a fifth of the clip's length of machine (a 5 s clip: 25 s, a 15 s one: 35 s). fine is the same gesture with a touch more grain, for about six times the week of fast (2 min 40, 8 min): ask for it when someone wants the finest on a take they keep, not to see what an upscale looks like. Any MiniMax definition can go there, a video already in full_hd goes no higher. It runs on the 1080p machines, not the render ones, and counts on the week at its GPU time; for the take you keep, not every take. Paid plans; limits.definitions of GET /v1/video/me lists full_hd when the plan has it.
Another take
A finished video you almost like: the same request rendered again with a new seed — the prompt and the first picture as the direction help left them, the same settings, nothing prepared or paid twice:
curl -X POST https://api.infinitepeople.ai/v1/video/jobs/$JOB_ID/retake -H "Authorization: Bearer $KEY"
# → a new creation, "edited_from": "$JOB_ID"; poll it like any other
It counts on the week like a render at that definition; 409 not_done on a video not finished, 409 no_take on a 1080p enlargement (ask a take of the video it was made from). On a video shared with you for editing, the take is yours and comes back to its owner shared. To change something in the request, send it again.
Render again after a failure
A job in status failed (the week got it back) is rendered again as it went to the render, the direction help's work kept and the same seed, so nothing is prepared or paid twice:
curl -X POST https://api.infinitepeople.ai/v1/video/jobs/$JOB_ID/retry -H "Authorization: Bearer $KEY"
# → a new creation, "edited_from": "$JOB_ID"; poll it like any other
It counts on the week like a launch; a job that did not fail answers 409 not_failed. The pictures of a request live 7 days: past that, send the request again.
Your videos, a word on one
GET /v1/video/jobs lists the account's videos newest first (the ones under way, then the finished ones for as long as they stay), then those shared with it (each with access and owner); ?limit= caps the list (200 at most). Each item is shaped like GET /v1/video/jobs/{id}; what is in the bin is not listed (GET /v1/creations?trash=1).
A word on a finished video, for us: POST /v1/video/jobs/{id}/feedback {"rating": 1..5, "comment": "…"} — one per account and per video, sending again replaces; GET the same path reads yours. GET /v1/video/queue/stats gives the depths of the queue (how many wait, how many render) when you want a hint before launching; POST /v1/video/estimate (above) says what one request would take.
See Projects to keep a video or a picture for good, and Assemble a montage to put clips together.
Make a song
A song is made in three steps: a quiz (optional) makes it personal, the director writes the words (and a judge reads them), you send the draft (edited or not) to be sung. One take per call; songs are kept 7 days like videos. Every take is a creation: fetch poll (it holds the connection up to 25 s and answers as soon as something changes; ?wait=0 answers at once), stop when ready is true or keep_polling is false; next says what to do.
1. The quiz
Fifteen short questions whose answers change the words the most (names, places, memories, the tone), three rounds of five, each with three choices and a free field; the first asks the style when the request names none. Streamed as they are written (the first within two seconds):
curl -N -X POST "https://api.infinitepeople.ai/v1/song/questions?stream=1" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"request": "une chanson pour les 30 ans de Léa, qui adore voyager"}'
# {"id": "r1q1", "round": 1, "text": "Quel style pour Léa ?", "choices": ["Pop lumineuse", "Chanson festive", "Folk acoustique"], "free_text": true, "placeholder": "Angèle"}
# …one line per question…
# {"done": true, "next": "Send the answers to /v1/song/write as 'answers' [{question, answer}]."}
Without stream, the whole list at once. Show five at a time (by round), add a "skip" yourself, and pass what was answered to /write as answers. A light cloud call, for subscribers.
2. The words
curl -N -X POST "https://api.infinitepeople.ai/v1/song/write?stream=1&review=1" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"request": "une chanson pop pour les 30 ans de Léa, qui adore voyager, de Lisbonne à Tokyo",
"answers": [{"question": "Quel style pour Léa ?", "answer": "Pop lumineuse"}]}'
# {"title": "Trente escales", "language": "fr", "style": "Angèle-like: first person, short lines, …"}
# {"line": "[Verse - intimate]"}
# {"line": "Elle a des cartes au fond des poches"}
# …one line per line of the lyrics, as they are written…
# {"draft": {"title": "…", "lyrics": "…", "caption": "pop, …, french", "duration_s": 170, "bpm": 118, "keyscale": "G major", "timesignature": 4, "words": 210, "instrumental": false}}
# {"fix": {"n": 7, "was": "Vers la maison", "now": "Vers l'horizon", "why": "trains / maison ne riment pas"}}
# …one line per line the judge changes…
# {"done": true, "draft": {…the fixes applied…}, "director": {…the director's draft…}, "review": {"scores": {"rhymes": 7, "meter": 8, "structure": 8, "singability": 8, "story": 9}, "summary": "…", "fixes": 3}, "next": "…"}
Two voices write it: a songwriter streams the words (a style line first: the writing it imitates), a producer sets the sound apart — the caption in English tags and the beat — so the story never tints the style. Name an artist in the request and the caption is built on that act's sound (the words stay original). review=1 has the judge read the draft in the same answer: n counts the non-blank lines of the lyrics from 1, tags included, now: "" removes a line; the closing draft has the fixes applied and director is the draft before them. Without stream, the draft at once (?review=1 adds director and review with its issues). Nothing is queued: the draft is yours to keep, edit or send back. Cloud calls, counted on the week.
To rework a draft, send it again with notes (streams the same way):
-d '{"request": "…", "previous": {…the draft…}, "notes": "one more verse about Lisbon, a sadder chorus"}'
"instrumental": true asks for no words: the songwriter draws the piece with section tags only (one or two instruments and the dynamics of each part) and the draft carries instrumental: true for /generate. With "music": "$ELEMENT_ID" (an element made with hear, see below) the director writes in the spirit of that track: its style, tempo and key, your words. The judge alone: POST /v1/song/review {"draft": …} (?stream=1 streams the fixes), answering issues, scores, summary and revised.
3. The music
curl -X POST https://api.infinitepeople.ai/v1/song/generate \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"title": "Trente escales pour Léa",
"lyrics": "[Verse - intimate]\nElle a des cartes au fond des poches\n…",
"caption": "pop, festive, female vocal, acoustic guitar, synthesizer, warm, catchy chorus, french",
"language": "fr",
"duration_s": 170,
"bpm": 118, "keyscale": "G major", "timesignature": 4,
"mode": "fast",
"lane": "normal"
}'
# → {"id": "…", "status": "queued", "lane": "normal", "ready": false, "keep_polling": true,
# "poll": "https://api.infinitepeople.ai/v1/song/jobs/…", "next": "Fetch 'poll' right now …", "estimate_seconds": 30}
When ready, result.url is the mp3, result.recipe_url a JSON next to it with the timed lyrics (lrc, one line per line of the lyrics, tags included) and what the song was made of, result.duration_s the measured length; ?complete=1 inlines the recipe. cover_url is the song's picture, painted from the title and the style while the song renders (subscribers; usually there before the song).
Before launching, POST /v1/song/estimate with the same body answers estimate_seconds (the render), wait_seconds (the queue ahead on that lane), total_seconds, modes (the render time of each mode for this request), and whether the plan and the week allow it — nothing is queued.
| field | values | |
|---|---|---|
lyrics | text, 4096 chars max | Section tags on their own lines ([Verse], [Chorus], [Bridge]…), with one or two performance hints in English after a dash ([Chorus - anthemic], [Bridge - whispered]). Tags only for an instrumental. |
caption | tags, 512 chars max | The style as comma-separated English tags: genre, mood, voice and its timbre, instruments, era and production, texture, structure, the language last. Not a sentence; reminiscent of <artist> points at a style. |
duration_s | 30 to 240 | Any length; 3 min on the free week and Starter, 4 min from Standard. The words are paced to fit. |
mode | fast, normal | fast (default) writes the song's blueprint with the light planner, about 30 s for 3 min; normal with the large one, richer composition, about twice the time (with a subscription). |
lane | priority, normal, fill | The queue: priority is served first and counts three times on the week, fill runs when a node is idle and counts a fifth less; normal by default. Every plan has the three lanes. |
language | fr, en, es, it, de, pt, nl, ja, ko, zh | The language of the vocals. |
bpm, keyscale, timesignature | 82, "D major", 4 | The beat; the director sets them, left out the planner guesses. |
title | text | Kept on the job for your library. |
instrumental | true / false | No vocals, whatever the lyrics say. |
seed | integer | Fix it to reproduce a take. |
voice, music | element ids | References, below. |
source, edit | a song id; repaint, extend, restyle | The editor, below. |
Statuses are queued, in_progress, done, failed, cancelled; a refusal carries code (not_on_plan, week_over, too_many_active, invalid_request…), a detail sentence and retry. DELETE /v1/song/jobs/{id} cancels or deletes like a video. Five creations in progress at most per account; thirty drafts an hour. Your week and what your plan allows in the songs: GET /v1/song/me → limits.modes, limits.max_duration_s, limits.lanes, limits.director, limits.cover. The catalogue and the licences: GET /v1/song/models.
4. Elements: a voice, a music
A track of yours, split on our machine into its voice and its music, to reference in a new song. Send a file (mp3, wav, flac, m4a, ogg, aac, 40 MB at most) with your word that you hold its rights or that it is free of rights; or split one of your finished songs.
curl -X POST https://api.infinitepeople.ai/v1/song/elements \
-H "Authorization: Bearer $KEY" \
-F "[email protected]" -F "title=Rescuer" -F "rights_confirmed=true" -F "hear=true"
# → {"id": "…", "kind": "element", "status": "queued", "ready": false, "keep_polling": true,
# "poll": "https://api.infinitepeople.ai/v1/song/elements/…", "estimate_seconds": 50}
curl -X POST https://api.infinitepeople.ai/v1/song/elements \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"from_job": "$SONG_ID"}' # one of your finished songs, no rights to confirm
When ready: result.voice (the voice alone), result.voice_ref (the 30 s where it sings the most, what a take uses), result.music (the track without the voice), result.heard (with hear: what the planner makes of the track — a description, bpm, keyscale, timesignature — for /write's music; about 25 s more; POST /v1/song/elements/{id}/hear adds it to a track split without it, as a new element). A separation of a 3 min track takes about 30 s of GPU. GET /v1/song/elements lists yours; GET /v1/song/elements/{id} holds the connection like a job; DELETE removes one with its files. Elements are kept 7 days.
Then, in /generate: "voice": "$ELEMENT_ID" gives the take that singer's timbre and manner (not the melody); "music": "$ELEMENT_ID" makes the take sing your words on that element's music (its structure, chords and rhythm; the length follows the music, the plan's limit applies; no planner, mode does not matter). Both together: your words, on that music, with that voice.
5. The editor
A finished song of yours, changed without starting over: "source": "$SONG_ID" and edit. Lyrics, caption, language and beat default to the source's; the answer is a new song with edited_from = the source, and the source's cover.
# a passage made again, with the words you send (the whole lyrics, changed where you want)
-d '{"source": "$SONG_ID", "edit": "repaint", "from_s": 62, "to_s": 80, "lyrics": "…"}'
# 20 seconds more at the end (add the lines that sing there)
-d '{"source": "$SONG_ID", "edit": "extend", "add_s": 20, "lyrics": "…\n\n[Outro - fade out]\n…"}'
# the same song in another style; fidelity 0..1 says how much of the original stays (0.5 by default)
-d '{"source": "$SONG_ID", "edit": "restyle", "caption": "80s synth-pop, drum machine, female vocal", "fidelity": 0.3}'
A repaint covers 3 to 90 seconds and leaves the rest untouched; an extension adds 5 to 60 seconds; the length of the result (the source's, plus add_s) must fit the plan. No planner in any of them: an edit takes about as long as a fast song.
Edit a picture
A picture of yours and your words: the engine reads the picture and paints a new one as you say, the person in it kept (the same face, features, age and build; everything else follows your words). No zones to draw. Optionally, a selection (the magic wand, a box, the lasso) restricts the change to it. A result stays as long as your account's retention says (retention and expires_at on the creation: a year by default, 48 h in the ephemeral mode, for good when permanent; the bin for 48 h after a deletion); the pictures you send, the masks and the engine's windows are tools' products, 7 days. The faces, the segmentation, the cutout and the enlargement run on our own machine (every account, the free week and after); the edit, the generation and the extension are cloud calls, counted on the week at what they cost (a plan with a week: the free week of Essentiel included; 402 trial_over once it is behind and nothing is subscribed).
1. Send the picture
curl -X POST https://api.infinitepeople.ai/v1/image/inputs \
-H "Authorization: Bearer $KEY" -F "[email protected]"
# → {"url": "https://media.infinitepeople.ai/ephemeral/image/inputs/u42/….jpg", "width": 1536, "height": 2752, …}
JPEG, PNG or WebP, 8 MB at most; moderated on the way in; stored upright (a phone's portrait on its side with an orientation tag is turned, the dimensions answered are the upright ones). A project's reference works as well (its URL is already ours).
2. The faces
curl -X POST https://api.infinitepeople.ai/v1/image/faces \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"image_url": "$URL"}'
# → {"faces": 1, "boxes": [[1033, 48, 1712, 536]], "cached": false, …}
How many faces the picture holds and where (about a second the first time, remembered an hour). It decides the engine of an edit: a face goes to the engine that preserves subjects, none to the fast one. Optional: pass faces to the edit to skip this look, or leave it out and the edit looks itself.
3. The edit
curl -X POST https://api.infinitepeople.ai/v1/image/edit \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"image_url": "$URL", "request": "a professional LinkedIn portrait, studio light, plain background", "shape": "portrait"}'
# → {"id": "…", "status": "queued", "kind": "transform", "engine": "gpt-image-2.5-sunburst", "engine_quality": "medium",
# "ready": false, "keep_polling": true, "poll": "https://api.infinitepeople.ai/v1/image/jobs/…",
# "next": "Fetch 'poll' right now; it holds the connection up to 25 s …", "estimate_seconds": 20}
The answer is a creation: fetch poll (it holds the connection up to 25 s and answers as soon as something changes; ?wait=0 answers at once). When ready, result.url is the picture the engine painted, whole (result.faces says how many faces it saw, result.enlarged_by whether our upscaler brought a large picture back to its size); ?complete=1 adds the prompt the engine got. Edit again from result.url, one change per turn; to go back, edit from lineage.previous_url (each creation carries its lineage: the root picture, the version, the previous creation; GET /v1/image/jobs?root=<url> lists the versions of a picture, oldest first).
| field | values | |
|---|---|---|
image_url | one of our URLs | The picture: an input, a project's reference, a result. |
request | text, 1000 chars | What the picture becomes, in your words. A new situation (a LinkedIn portrait, at the beach) or a change to this very picture (black and white, a hat): say it plainly, the person stays who they are. |
shape | square, landscape, portrait, wide, WxH | The result's shape when it differs from the picture's own (a portrait from a landscape). |
references | up to 4 of our URLs | Pictures giving the identity of a person or the look of an object. |
faces | a number | From POST /faces, to skip the look. |
quality | low, medium | The mode. low (default) spends little: our own engine edits your picture (GPU seconds), gpt-image-2.5 at its low tier makes one from words. medium: gpt-image-2.5 medium for both, about two cents. auto and draft read as low, keep and high as medium. |
mask_url / box / polygon | optional, one | The mask mode (below). |
director | true / false | On by default: the director looks at the picture and writes the engine's instruction from your words, naming what changes as it is in the picture, what the new thing looks like and what stays (a small cloud call; the record's director says which model answered and what it cost). Send false to give the engine your words as they are. |
lane | priority, normal, fill | How the week counts it (×3, ×1, ×0.5); the plan's default when absent. |
confidentiality | low, medium, high | The account's default when absent; high keeps the local tools only (no cloud engine). |
Two modes (quality). low, the default, the one that spends little: a picture you edit goes to our own engine, Qwen-Image-Edit-2511 on our machine (engine_quality: local, counted in GPU seconds), and a picture made from words to gpt-image-2.5 at its low tier. medium: gpt-image-2.5 at its medium tier for both. Whatever the mode, when gpt-image-2.5 cannot answer the creation goes to qwen-image-3.0 on Alibaba Model Studio, and when our machine cannot take an edit it goes to the cloud engine of the mode — the job's engine, engine_quality and route_reason always say which one served. Between the two gpt engines: Sunburst, the one that preserves subjects, when the picture holds a face (faces, from POST /v1/image/faces) or a reference is given, or when nobody looked; Flare, the fast one, otherwise. The director writes the instruction from your picture and your words by default. POST /v1/image/estimate with the same body tells the engine, the seconds and what the week is asked, without launching anything; GET /v1/image/me → service.engine says whether our machine is up. (auto and draft read as low, keep and high as medium.)
The mask mode: only a selection changes
curl -X POST https://api.infinitepeople.ai/v1/image/edit \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"image_url": "$URL", "mask_url": "https://media…/masks/….png", "request": "an ambulance in place of the taxi"}'
With a selection — a mask from the magic wand (POST /v1/image/segment/open then /segment/tap {"session", "points": [[x, y]]}: the object under a tap, best mask first; /segment/concept {"text"} for every instance of a word; /segment/scene for everything that matters), a box [x0, y0, x1, y1] or a polygon [[x, y], …] (the lasso) in the picture's pixels — only the selection changes (kind: edit): the engine paints a window around it and the change is composited back into your picture, so nothing outside moves. A replacement takes exactly the place, size and perspective of what it replaces. result.window_url is the engine's own output of the window, result.mask_url the selection, result.used_mask_url what came back; fit says how: smart (default) keeps your picture as it is as far as possible — the selection plus what the engine clearly changed around it (a jacket asked over a t-shirt comes back whole; spill says by how much) — selection the selection alone, none the engine's whole window. A selection covering the whole picture is the whole picture. The masks come cleaned of the model's specks and inline (mask_png_b64) on a tap.
The object alone
curl -X POST https://api.infinitepeople.ai/v1/image/cutout \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"image_url": "$URL", "mask_url": "https://media…/masks/….png"}'
# → {"url": "https://media…/cutouts/….png", "width": 679, "height": 1488, "box": [1033, 48, 1712, 1536]}
A PNG with transparency, cropped to the object ("crop": false keeps the frame), a soft edge of feather_px (2 by default). Local, immediate, free of the week.
A picture from words
curl -X POST https://api.infinitepeople.ai/v1/image/generate \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"request": "a ceramic mug on a wooden table by a window at dusk", "shape": "landscape", "style": "a photograph"}'
A creation like an edit. shape: square (1024²), landscape (1344×768, default), portrait, wide (1536×640), or a WIDTHxHEIGHT on multiples of 16 (655 360 px at least, no side above 2048: enlarge afterwards). references (up to 4) give the identity of the people or the look of the objects in them, never the composition. The same quality, director, lane, confidentiality as an edit.
A wider frame
curl -X POST https://api.infinitepeople.ai/v1/image/extend \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"image_url": "$URL", "shape": "square", "request": "more of the room"}'
The scene continued beyond the edges into a new frame: a shape (the picture's longer side is kept) or a factor (1.05 to 2, both sides); result.box says where your picture sits, never scaled. request is optional.
The engine redraws the whole frame, your picture included: at its native size the redrawing keeps the composition but drifts by a few pixels and reinvents textures; a large photo it had to paint smaller comes back recomposed. So fit says how your picture comes back, as for an edit: smart (default) keeps it as it is as far as possible — the engine's pixels are taken where it clearly changed things, connected to the new bands, yours everywhere else, the seam where both agree (a flowerpot or a leg at the edge is not doubled); selection pastes your picture back whole, pixel for pixel (a seam can show where the engine drifted); none is the engine's frame as it is. result.kept is the share of your picture that came back untouched (1.0 = all of it; a large photo may keep a fifth), result.used_mask_url where the engine's pixels were taken, result.window_url its frame as it painted it. A frame painted smaller than asked is enlarged by our upscaler first (result.enlarged_by), not by a resize.
Larger
curl -X POST https://api.infinitepeople.ai/v1/image/upscale \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"image_url": "$URL", "factor": 2}'
×2, ×3 or ×4 on our own machine, every plan: a second or two for a megapixel, ten for a 4K result, counted in GPU seconds. No diffusion: a photo stays the same photo, larger (its file says AI-upscaled - 4xNomos - Infinicia in its metadata, like every file we produce). "mode": "fine" is a fidelity notch (a transformer, five times slower, more careful with a noisy or compressed photo; on a clean picture fast is as sharp). Source of at most 3840×2160.
Every creation of the trade is read on GET /v1/image/jobs/{id} and listed on GET /v1/image/jobs (a bare list: yours newest first, then the ones shared with you, each shaped like GET /jobs/{id}); DELETE /v1/image/jobs/{id} cancels one that is not done (counted if the engine was already called: the answer says charged) or puts a finished one in the bin (GET /v1/creations?trash=1, POST /v1/creations/{creation_id}/restore for 48 hours; a creation in the bin answers 404 in_bin with its creation_id). Every record carries creation_id, the registry's id, retention and expires_at, and access: owner, or the role a share gives you (POST /v1/creations/{creation_id}/shares {"role": "view" | "edit"} hands one person a one-time link; GET /v1/creations?shared=1 lists what is shared with you) with owner: {id, name} — view reads the record and the picture, edit also lets you edit, widen or enlarge from its result.url (the new creation is yours, on your week, in the picture's lineage, and comes back to its owner shared for editing); a view-only share that launches gets 403 share_view_only, anyone but the owner who deletes 403 not_owner. Five in progress at most per account. Your week and what your plan allows in the pictures: GET /v1/image/me → limits.edit, limits.qualities, limits.upscale. The catalogue: GET /v1/image/models.
| code | |
|---|---|
404 session_expired | the segmentation session is over (15 minutes) or not yours: open the picture again |
503 segmentation_busy | the segmentation card is busy for retry_after_s; a box or a polygon needs no model meanwhile |
422 empty_mask | the mask selects nothing |
job failed, refused_by_engine | the engine's safety layer refused the edit; the week was given back — change the request or the selection |
job failed, unworkable_selection | the selection is more than 3:1 within the picture; widen it |
429 engine_busy | the engine has done its share of pictures this hour for everyone; later |
403 share_view_only | the picture is shared with you for viewing only; its owner can give you edit access |
403 not_owner | only the owner deletes a creation shared with you |
404 in_bin | the creation is in the bin; POST /v1/creations/{creation_id}/restore within 48 hours |
Make a presentation
A page written by our own model, on our machines, from a sentence: slides, a one-page site, or a live quiz. The page is a single HTML file on our kit (themes, layouts, components, fonts), served by its id, sandboxed; it lives 7 days. Counted on the week in GPU seconds at the node's rate, every plan.
curl -X POST https://api.infinitepeople.ai/v1/deck \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"prompt": "An eight-slide pitch for investors: the problem, our answer, the market, the team, the ask.", "shape": "deck", "theme": "night"}'
# → {"id": "…", "status": "queued", "ready": false, "keep_polling": true, "poll": "https://api.infinitepeople.ai/v1/deck/jobs/…",
# "next": "Fetch 'poll' right now; it holds the connection up to 25 s …", "estimate_seconds": 33, "version": 1, "root_id": "…"}
Fetch poll (held up to 25 s; ?wait=0 answers at once). When ready, result.url is the page — open it, share it, put it in an <iframe sandbox="allow-scripts">; result.title, result.shape and result.slides describe it; ?complete=1 adds the HTML itself as page. The page is public to whoever holds its address (the id is the secret).
| field | values | |
|---|---|---|
prompt | text, 3 to 3000 chars | What the presentation says, in your words: the subject, the audience, the tone, how many slides. |
shape | deck, site, quiz | Slides to present; a one-page site to send; a quiz to play live (questions, answers, a scoreboard). |
theme | night, paper, ocean, cinema, kids, forest, warm, mono, candy, slate | Left out, our model picks one for the subject. The kit's palette: GET /v1/deck/kit. |
template | an id of GET /v1/deck/kit → templates | A starting structure for the shape (a pitch, a lesson, a launch; a showcase, a report, an event; three quizzes). GET /v1/deck/kit/templates/{id} shows one as a page (?theme= to try a theme). |
media | up to 12 of {"url", "kind": "image" | "video" | "audio", "title"} | Files of ours (a project's picture, a video, a song) our model may place; title is what it reads about each. |
language | fr, en… | The page's language; the prompt's when left out. |
lane, confidentiality | as everywhere | The presentations are written on our own machine: available at every confidentiality level, high included. |
Rework it
curl -X POST https://api.infinitepeople.ai/v1/deck/jobs/$ID/edit \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"instruction": "Add a slide on pricing after the market, and make the accent orange."}'
A new creation in the lineage (edited_from, root_id, version n+1); the previous page stays. POST /v1/deck/estimate {"shape", "template?", "lane?"} tells the seconds and what the week is asked without launching; GET /v1/deck/jobs lists your presentations; DELETE /v1/deck/jobs/{id} cancels one (counted if our model was already writing: charged) or deletes a finished one. Your rights: GET /v1/deck/me → limits.shapes, limits.media_max; service.online says whether our model is up.
| code | |
|---|---|
422 invalid_request | an unknown shape, theme or template; a medium that is not on our media host |
409 not_a_finished_page | the page to rework is not finished yet |
job failed, page_refused | our model could not make an acceptable page; the week was given back — rephrase |
job failed, engine_unavailable | our model's node is down; try again in a minute |
job failed, source_missing | the page to rework has expired |
Give a voice
A voice kept on your account — yours, recorded, or one designed from words — that says your texts and takes over your recordings. Everything runs on our own machine: saying at 48 kHz (VoxCPM2), converting at 24 kHz (CosyVoice 3). Every plan, counted on the week in GPU seconds; what a creation makes is kept as long as your account's retention says (a year by default; retention on the request chooses for one creation; deleting puts it in the bin for 48 hours), a kept voice until you delete it. A text is moderated before it is said.
1. Keep a voice
curl -X POST https://api.infinitepeople.ai/v1/voice/voices \
-H "Authorization: Bearer $KEY" \
-F "name=Nicolas" -F "[email protected]" -F "rights_confirmed=true" \
-F "transcript=Bonjour, je m'appelle Nicolas. Je lis ce texte pour donner ma voix au Studio…"
# → {"id": "61e7…", "name": "Nicolas", "kind": "recorded", "sample_url": "https://media.infinitepeople.ai/voices/library/u42/….m4a",
# "transcript": "Bonjour, je m'appelle Nicolas. …", "duration_s": 28.8, "faithful": true, "next": "Say a text with it: …"}
A recording of one person speaking, 3 to 60 s (the engine reads the first 30; ten to thirty of natural reading is best), wav, mp3, m4a, aac, ogg, webm or flac, 40 MB at most, with the word on the rights: rights_confirmed says it is your voice, or you hold the rights on it. Give transcript, the exact text read, when you have it: the voice is then faithful — the engine continues your recording, the closest cloning (a swallowed word or a hesitation is fine; a text read in part is not: a transcript that does not fit the recording's length is set aside, and next says so). Without it, an engine makes one (qwen3-asr-flash on Alibaba Model Studio, Whisper as the fallback: a cloud call, counted on the week at its cost; not at the high confidentiality level, not once the free week is behind without a plan); otherwise the voice is cloned from its timbre alone (faithful: false), still the same voice, a little freer. language (fr, en…) helps the transcript; gender (female | male) says the voice's register, for a mood's actor.
curl -X POST https://api.infinitepeople.ai/v1/voice/voices \
-H "Authorization: Bearer $KEY" \
-F "name=Conteuse" -F "description=a woman in her forties, warm low voice, a little amused, a Parisian accent" -F "language=fr"
# → {"id": "1193…", "kind": "designed", "sample_url": "…/voices/library/u42/….wav", "transcript": "Bonjour, voici ma voix. …", "duration_s": 13.0, …}
Or a voice from words, drawn several times: POST /v1/voice/designs {"description": "une femme d'une cinquantaine d'années, voix grave et lente", "language": "fr", "takes": 3} answers one take per draw (seed, duration_s, sample_url), each saying the same sample of about fifteen seconds. The draw decides, not the language: the same words give a take that obeys and one that drags, so listen and keep the one you like — POST /v1/voice/voices with name, its design_url and the same description, and it is kept exactly as you heard it (nothing rendered again, nothing counted twice). The whole draw is counted once on the week (a few GPU seconds a take); the takes nobody keeps go with the week's cleaning. One shot still works (description straight to /voices, seed to repeat a draw). Thirty voices per account; GET /v1/voice/voices lists them (yours, then the ones shared with you), GET …/voices/{id} is one, PATCH …/voices/{id} {"name"} renames it ({"gender"} sets its register), DELETE …/voices/{id} puts it in the bin (48 hours to take it back, GET /v1/creations?trash=1; then its sample is gone; the creations made with it stay). A kept voice is a creation of the registry too (creation_id on its record, kind voice, kept for good): share it like any creation — POST /v1/creations/{creation_id}/shares {"role": "view" | "edit"} makes a one-time link; the person who takes it hears the voice (view) or says texts with it and converts recordings into it (edit, on their own week); every record then says access and owner. What is said in a shared voice comes back to the voice's owner, shared for editing: you hear everything made with your voice. Withdraw a share at any time (DELETE …/shares/{share_id}); the bin ends them all.
2. Say a text
curl -X POST https://api.infinitepeople.ai/v1/voice/say \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"voice": "61e7…", "text": "Bonjour à tous, et bienvenue dans Infinicia.", "seed": 11}'
# → {"id": "…", "kind": "say", "status": "queued", "ready": false, "keep_polling": true, "poll": "https://api.infinitepeople.ai/v1/voice/jobs/…",
# "next": "Fetch 'poll' right now; it holds the connection up to 25 s …", "estimate_seconds": 2}
A creation: fetch poll (held up to 25 s; ?wait=0 answers at once). When ready, result.url is the speech (WAV, 48 kHz, result.duration_s long), retention and expires_at how long it stays, result.seed the seed it used: the same text with the same seed says it the same way, another seed is another take. A long text is fine (6 000 characters at most, about a minute of speech for 900): the engine says it by sentence groups, each continued from the voice and the previous group, so the voice holds from the first line to the last; a minute of speech renders in fifteen to twenty seconds. denoise cleans a noisy recording before cloning (a phone in a street). The delivery is the voice's own — unless you ask a mood: joy, sadness, calm, whisper, enthusiasm or tenderness (anger is not offered: the engines do not play it well). The text is then played by an actor designed in that mood, of the voice's register (gender on the kept voice: given, or read in a designed voice's description), and converted into the voice: same words, the actor's intention, your voice's timbre (24 kHz, about twice the GPU seconds of a plain take).
3. Play it yourself: the conversion
curl -X POST https://api.infinitepeople.ai/v1/voice/convert \
-H "Authorization: Bearer $KEY" \
-F "voice=1193…" -F "[email protected]" -F "rights_confirmed=true"
# → a creation; when ready, result.url is the recording said again in the voice (WAV, 24 kHz)
Record the line yourself, with the pace and the intention you want; the kept voice takes it over word for word, same rhythm, same intention — the way to have an emotion in a voice. The source comes as file (with rights_confirmed: it is you, or you hold the rights), as file_url on our media host (a song's voice element, a project's asset; no word needed), or as source, a finished creation of the trade — yours, or one shared with you for editing (edited_from on the new record). Two minutes at most; about 0.6 s of GPU per second of recording. steps (4 to 64, 30 by default) is the conversion's diffusion steps: fewer is faster, 30 is cleaner on expressive sources.
| field | values | |
|---|---|---|
voice | an id of GET /v1/voice/voices | The kept voice that says, or takes over. |
text | text, 1 to 6000 chars | What is said (say only). |
seed | integer | The same seed, the same take (say and design). |
mood | joy, sadness, calm, whisper, enthusiasm, tenderness | The delivery, played by an actor and converted into the voice (say only; 24 kHz). |
file / file_url / source | a recording / one of our URLs / a creation's id | The source to convert (convert only); a file needs rights_confirmed; a creation is yours or shared with you for editing. |
lane | priority, normal, fill | How the week counts our GPU (×3, ×1, ×0.8); every plan has the three; normal by default (priority on Pro, at cost). The voices run on our machine, so the lane is the whole of it; a cloud call (a recording's transcript) is counted at its price whatever the lane. |
confidentiality | low, medium, high | Saying, designing and converting run on our own machine: available at every level. At high the transcript (a cloud call) and the moderation of a text are out, so a voice is kept from its timbre and a text cannot be said yet; converting works. |
retention | standard, ephemeral, permanent | How long this creation stays (a year; 48 hours then gone for good; until deleted); the account's default when absent. Kept in a project, it does not expire meanwhile. |
Every creation of the trade is read on GET /v1/voice/jobs/{id} (?complete=1 adds the voice's transcript as sent) and listed on GET /v1/voice/jobs (a bare list: yours newest first, then the ones shared with you); DELETE /v1/voice/jobs/{id} cancels one that is not done (counted if the node was already working: charged) or puts a finished one in the bin (GET /v1/creations?trash=1, POST /v1/creations/{creation_id}/restore for 48 hours; a creation in the bin answers 404 in_bin with its creation_id). Every record carries creation_id, the registry's id, and access: owner, or the role a share gives you (POST /v1/creations/{creation_id}/shares {"role": "view" | "edit"} hands one person a one-time link; GET /v1/creations?shared=1 lists what is shared with you) with owner: {id, name} — view reads the page and the file and keeps it in a project of yours, edit also converts it into a voice of yours (the conversion is yours, on your week, and comes back to its owner shared for editing); a view-only share that converts gets 403 share_view_only, anyone but the owner who deletes 403 not_owner. Five in progress at most per account. Your week and what your plan allows: GET /v1/voice/me → limits.transcript, limits.max_text_chars, limits.max_source_s. The engines: GET /v1/voice/models.
| code | |
|---|---|
422 rights_required | a recording sent without rights_confirmed |
422 moderation_refused | the name, the description or the text was refused |
409 too_many_voices | thirty voices are kept already; delete one |
503 voice_busy | the voice card belongs to a character generation for retry_after_s (a design is immediate: call again then; a creation waits by itself) |
job failed, voice_unavailable | the node is unreachable; the week was given back — send it again in a minute |
job failed, invalid_input | the recording could not be read, or the text could not be said |
Projects
A project keeps for good what the studio makes — pictures, videos, songs and elements (a voice, a music), texts said and recordings converted — and the montages assembled from them; everything else lives 7 days. Every item has a handle (picture:5, video:12, song:3, element:7:voice): the word the bench takes.
curl https://api.infinitepeople.ai/v1/projects/$PROJECT -H "Authorization: Bearer $KEY"
# → {"id": 12, "name": "Mia",
# "references": [{"id": 5, "handle": "picture:5", "url": "https://media.infinitepeople.ai/videos/references/u42/….png"}],
# "videos": [{"id": 12, "handle": "video:12", "title": "The yellow taxi under the rain", "url": "…/videos/projects/u42/….mp4", "poster_url": "…", "duration_s": 5.0, "job": {…}}],
# "songs": [{"id": 3, "kind": "song", "handle": "song:3", "title": "Trente bougies", "files": {"url": "…"}, "duration_s": 170},
# {"id": 7, "kind": "element", "handles": {"voice": "element:7:voice", "music": "element:7:music"}, "title": "Léa", "files": {"voice": "…", "music": "…"}, "duration_s": 182}],
# "montages": [{"id": 4, "name": "Night", "version": 9, "duration_s": 23.5, "last_render": {…}, "page_for_your_human": "https://infinicia.ai/#montage:12:4"}], "montage_count": 1,
# "page_for_your_human": "https://infinicia.ai/#project:12", "next": "…"}
GET /v1/projectslists them;POST /v1/projects{"name": "Mia"}makes one;PATCH …/{id}{"name"}renames it;DELETE …/{id}removes it with everything it keeps, files included.- A picture:
POST …/{id}/references(multipartfile, jpeg/png/webp, 8 MB at most) orPOST …/{id}/references/from-url{"url"}for one already on our media host (a frame, an upload, an edited picture). Moderated on the way in.DELETE …/references/{rid}. - A video:
POST …/{id}/videos{"job_id"}copies a finished video where it does not expire and answers it with ajobobject shaped likeGET /v1/video/jobs/{id}(result_urlis the permanent copy);DELETE …/videos/{vid}. A frame (…/jobs/{id}/frame) of a kept video still works after the job record is gone. - A song or an element:
POST /v1/song/projects/{id}/assets{"job_id"};DELETE …/assets/{aid}. - Anything else of yours — an edited picture, a presentation:
POST /v1/projects/{id}/assets{"trade": "image" | "deck", "job_id"};DELETE /v1/projects/{id}/assets/{asset_id}. Nothing is copied: the project holds the creation, which then does not expire while it is there (held: true, noexpires_at); taken out, it lives its own retention again. - A voice creation (a text said, a recording converted):
POST /v1/voice/projects/{id}/assets{"job_id"};GET …/assets;DELETE …/assets/{aid}.
GET /v1/projects/{id}/assets answers everything the project keeps with the same words for each — handle, trade, job_id, kind, title, urls by role, duration_s, and how long it stays (retention, held, expires_at, creation_id) — pictures you uploaded (retention: "project") included. An edited picture the project keeps can start a shot on the bench ("picture": "image:4").
Limits: 30 projects on an account; 60 pictures, 30 videos, 60 songs and elements, 60 voices, 20 montages in a project (409 project_full).
Assemble a montage
The bench. A montage is a document of a project — a sequence of video clips with a transition between each, a voice track and a music track placed in time — that you change by operations and export as an mp4. What the app's bench does, you do here with the same words.
A montage is named by its id alone (/v1/montages/{id}/…); it lives in a project — POST /v1/projects/{id}/montages — or without one: POST /v1/montages {"name"} puts it in the account's inbox, a project of kind inbox (one per account, neither renamed nor deleted) where what the montage uses is kept for good like anywhere else. GET /v1/montages lists every montage of the account with its project; PATCH /v1/montages/{id} {"project_id", "name"?} moves one into a project, its clips keeping their files.
curl -X POST https://api.infinitepeople.ai/v1/projects/$PROJECT/montages \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"name": "Night", "aspect": "16:9"}'
# → {"id": 4, "version": 1, "doc": {…}, "summary": {"duration_s": 0, "video": [], "voice": [], "music": []},
# "palette": [{"handle": "video:12", "kind": "video", "title": "The yellow taxi under the rain", "duration_s": 5.0, "url": "…"},
# {"handle": "song:3", "kind": "music", "title": "Trente bougies", "duration_s": 170, "url": "…"},
# {"handle": "element:7:voice", "kind": "voice", "title": "Léa", "duration_s": 182, "url": "…"}],
# "page_for_your_human": "https://infinicia.ai/#montage:12:4",
# "next": "Empty montage, version 1. Place clips with POST …/ops {\"ops\": [{\"op\": \"add\", \"track\": \"video\", \"from\": \"video:<id>\"}], \"version\": 1} …"}
Then operations, applied in order, all or none, one save:
curl -X POST https://api.infinitepeople.ai/v1/montages/4/ops \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"version": 1, "ops": [
{"op": "add", "track": "video", "from": "video:12"},
{"op": "add", "track": "video", "from": "job:2f3a5b7c-…", "out": 3},
{"op": "add", "track": "music", "from": "song:3", "start": 0, "out": 8, "value": 0.5},
{"op": "transition", "id": "c1a2b3", "kind": "fade", "s": 0.5}]}'
# → {"id": 4, "version": 2,
# "done": ["'Rain at night' kept in the project", "'The yellow taxi under the rain' added to the video track at place 1", "'Rain at night' added to the video track at place 2", "'Trente bougies' added to the music track at 0 s", "'The yellow taxi under the rain' now fades into the next clip over 0.5 s"],
# "summary": {"duration_s": 7.5, "video": [{"id": "c1a2b3", "title": "The yellow taxi under the rain", "start": 0, "length": 5, "transition": "fade", "from": "video:12"}, {"id": "c4d5e6", "title": "Rain at night", "start": 4.5, "length": 3, "transition": "cut", "from": "video:13"}], "voice": [], "music": [{"id": "a7b8c9", "title": "Trente bougies", "start": 0, "length": 8, "gain": 0.5, "from": "song:3"}]},
# "doc": {…}, "next": "Version 2: send it as 'version' with your next operations …"}
| op | fields | |
|---|---|---|
add | track video / voice / music; from a handle (or src, a source copied from the palette); in, out seconds into the source (out defaults to its length); start audio: seconds on the montage's clock; index video: its place, 0-based; value audio: volume 0..2 (music 0.6 by default) | a clip on a track |
remove | id | |
move | id; index (video) or start (audio) | |
trim | id; in, out | seconds into the source |
split | id; at seconds on the montage's clock | the clip becomes two |
transition | id; kind cut / fade; s seconds | from this video clip to the next |
gain | id; value 0..2 | voice or music |
fade | id; fade_in_s, fade_out_s | voice or music |
rename | id; title | |
replace | id; from (or src) | another source in the same place, bounds and transition kept |
aspect | aspect 16:9, 9:16, 1:1, 4:3, 3:4, 21:9 |
Handles: video:12, song:3, element:7:voice, element:7:music name what the project keeps (GET /v1/projects/{id}); job:<uuid>, song:<uuid>, element:<uuid>:voice name a finished creation of the account, kept in the project on the way, so the montage never points at a file that expires. Video clips play one after the other in the order of summary.video; a fade pulls the next clip back by its length. Voice and music clips are free: each has a start. Every other operation names its clip by the id shown in summary.
version is optional but wise: the version you read, sent back, makes a change made elsewhere meanwhile a 409 montage_stale (the body says the current version) rather than a silent overwrite. GET /v1/montages/{id} reads the montage again with summary and palette; PUT /v1/montages/{id} {"doc", "version"} replaces the whole document; DELETE /v1/montages/{id} removes it with its exports.
A sentence instead of operations: POST /v1/montages/{id}/direct {"prompt": "put Trente bougies under the whole montage and cut the taxi at 3 s"} — our director turns it into operations, applied all or none; done says each, reply its word, refused why not. It costs a small cloud step on the week; the operations you send yourself are free.
A video that does not exist yet: POST /v1/montages/{id}/make {"shots": [{"prompt": "Mia hails a yellow taxi in the rain", "duration_s": 5, "index": 0, "picture": "picture:5", "title": "The taxi"}]} — up to 5 shots, each a video creation on the week like any other (the montage's frame; picture starts it on a picture of the project; index its place, the end when absent), placed on the video track by itself when ready; until then the montage's pending lists them with their poll, and done says when one landed or could not be made. The director does the same from a sentence (“add a shot of the beach at dawn after the taxi”): its answer carries the shots in make, not made until you send them to …/make — or pass "make": "launch" to /direct to make them at once.
Sharing (the studio's rule for every creation): an export is a creation — POST /v1/creations/{creation_id}/shares {"role": "view" | "edit"} hands one person a link; taken, it opens the montage to them: GET /v1/montages/{id} answers its card with access, owner and shared_renders (with the document for edit), GET /v1/montages/{id}/renders/{rid} the export to watch and download; GET /v1/montages lists the montages shared with you after your own. With edit, POST /v1/montages/{id}/fork {"name"?, "project_id"?} makes your own montage from it (your week pays its exports, which come back to the owner shared for editing); the owner's montage itself is changed by the owner only (403 not_owner, share_view_only for a viewer).
Export: POST /v1/montages/{id}/render answers a creation (poll, ready, keep_polling, next); ready, result.url is the mp4 and result.poster_url its first frame, kept with the project (last_render on the montage). Limits: 40 clips, 5 minutes.
Touch up a clip's picture, as the app's object tool does: take the frame (POST /v1/video/jobs/{job_id}/frame {"t"}), edit it (POST /v1/image/segment/open, /tap, /edit), make the clip again from the corrected picture (POST /v1/video/generate with first_frame_url and "edited_from"), then {"op": "replace", "id": "c1a2b3", "from": "job:<the new video>"}.
Statuses
| status | |
|---|---|
enhancing | waiting for, or under, the direction help |
review | the painted first image awaits your say (review: true); it goes on by itself after review_deadline |
queued | in line for a rendering node |
in_progress | rendering; estimate_seconds is the expected render time |
done | result_url is ready |
failed, cancelled | the week gets the video back (unless cancelled while rendering); POST …/jobs/{id}/retry renders a failed one again |
DELETE /v1/video/jobs/{job_id} cancels a job (free before the render, counted once a node has it) or puts a finished video in the bin (48 h to restore it, POST /v1/creations/{creation_id}/restore).
Refusals
Every refusal is a status and a JSON body: detail is a sentence that says what to do and whether retrying makes sense, code a stable word, retry a boolean (with retry_after_s when waiting helps): {"detail": "This account's week is used up…", "code": "week_over", "retry": false, "resets_at": "…"}. A 4xx without a JSON body is not ours.
| code | |
|---|---|
402 not_on_plan | the length or the direction level is not on your plan |
402 week_over | your week is spent; resets_at says when it starts again |
422 | the request or a picture was refused by moderation, or a field is wrong (invalid_request: the detail says which) |
422 not_available_at_confidentiality | the confidentiality asked (low by default, medium, high) cannot do a step of the request; refused names the option to change (the direction help needs a cloud service) — high is not open yet |
404 not_found | not there, or not yours: a project, a montage, a handle, a job |
409 montage_stale | the montage changed since the version you sent; version says the current one — read it again |
409 project_full | the project holds its most of that (pictures, videos, songs, montages) |
429 | ten videos already in progress on the account |
503 | the service is paused for maintenance; try again later |
Tell us
A bug, a wish, a word on a creation: POST /v1/studio/feedback with {"subject": "api" | "video" | "song" | "image" | "deck" | "voice" | "app" | "other", "message": "…", "job_id": "…"} (the job is optional). We read every one. The rating of a video (1 to 5 and a comment) stays on POST /v1/video/jobs/{job_id}/feedback.
For machines
The OpenAPI document of every route a key can reach: api.infinitepeople.ai/v1/studio/openapi.json. This page in Markdown: /api/index.md. A summary for language models: /llms.txt.
MCP. The same routes as tools, from a conversation: https://api.infinitepeople.ai/v1/mcp/crea (JSON-RPC 2.0 over HTTP, POST only, your key as the bearer). The tools are named verb_object (list_projects, get_project, keep_video, create_montage, edit_montage, direct_montage, render_montage, make_video, get_video, write_song, make_song, get_song, split_song…), one parameter per field of the route, and answer what the route answers (a refusal is a result with isError). In Claude Code:
claude mcp add --transport http infinicia https://api.infinitepeople.ai/v1/mcp/crea \
--header "Authorization: Bearer $KEY"
Then say what you want: “make a 5-second video of a yellow taxi in the rain, keep it in my project Mia, put it on a new montage with my song Trente bougies under it, and export”. Any MCP client that sends a bearer header works the same way.
Add Infinicia to Claude (claude.ai, desktop, mobile). No key to paste: in Claude, open Settings › Connectors › Add custom connector, give it the URL https://api.infinitepeople.ai/v1/mcp/crea, then connect — a page of the studio opens, you sign in with Google, and Claude holds a key of your account (scope studio, named “Claude (connecteur)”, shown and revocable in the app under Account › API access; it renews itself, and everything Claude makes counts on your week as in the app). Claude Code and other MCP clients that speak OAuth 2.1 (PKCE S256, dynamic registration or a client metadata document) go through the same door: https://api.infinitepeople.ai/.well-known/oauth-authorization-server says where.
Your week
curl https://api.infinitepeople.ai/v1/video/me -H "Authorization: Bearer $KEY"
week.left_pct is the share of your week that is left, week.resets_at when it starts again, limits what your plan allows. The catalogue with every field: GET /v1/video/models.
The whole account in one read: GET /v1/studio/me — user (your name, your picture's URL), plan (Gratuit, Essentiel, Starter, Standard, Pro), week, limits and services per trade, retention (what your next creations get: standard, a year; ephemeral, 48 hours; permanent). Your name as others see it on a shared creation: POST /v1/studio/profile {"name": "…"} (2 to 40 characters); your picture: POST /v1/studio/avatar (multipart file, JPEG, PNG or WebP up to 5 MB, cropped to a square) and DELETE /v1/studio/avatar. The plans and their prices: GET /v1/studio/plans?currency=EUR.