---
title: Infinicia API
description: Make videos, songs, pictures, presentations and voices from your own tools with a personal key, keep them in projects and assemble them on the bench; every creation counts on the key owner's week.
canonical: https://infinicia.ai/api/
openapi: https://api.infinitepeople.ai/v1/studio/openapi.json
---

# Infinicia API

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.

In a conversation instead of a script? The same studio is an MCP server: https://infinicia.ai/mcp/ — add it to Claude, ChatGPT or Cursor and the assistant does all of this for the person, on their account.

## Your key

Create one on the app's [Account](/#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 "file=@mia.jpg"
# → {"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](#projects) to keep a video or a picture for good, and [Assemble a montage](#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 "file=@track.mp3" -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 "file=@photo.jpg"
# → {"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 from a sentence: slides to present, a one-page site to send, or a quiz to play live with the room's phones. The page is a single HTML file on our kit (fifteen themes, nine layouts, twenty-two components, the fonts), served by its id, sandboxed; it stays as long as your account's retention says (a year by default; deleting puts it in the bin for 48 hours). Two qualities: `rapide` (the default) is written by our own model on our machines, counted on the week in GPU seconds at the node's rate; `soignee` by a larger model at a provider, counted at its cost. 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": "studio"}'
# → {"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`, `result.slides` and `result.poster` (a picture of its cover) describe it; `result.engine` says which model wrote it and `result.voice` what the editor found; `?complete=1` adds the HTML itself as `page`. The page is public to whoever holds its address (the id is the secret). The record also answers `creation_id`, `retention`, `expires_at`, and `access` (`owner`, `edit` or `view` when it was shared with you, then `owner {id, name}`).

| 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, points for speed, a ranking). |
| `theme` | `studio`, `studio-dark`, `magazine`, `atelier`, `neon`, `night`, `paper`, `ocean`, `cinema`, `kids`, `forest`, `warm`, `mono`, `candy`, `slate` | Left out, our model picks one for the subject (`studio` when nothing says otherwise). 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. |
| `documents` | up to 5 ids from `POST /v1/deck/documents` | Your own documents as the material: the facts, the figures, the wording come from them. `POST /v1/deck/documents` takes a multipart `file` (PDF, DOCX, PPTX, TXT, MD; 5 MB) → `{id, title, words, pages, truncated}`; kept 7 days. |
| `language` | `fr`, `en`… | The page's language; the prompt's when left out. |
| `quality` | `rapide`, `soignee` | Our own model on our machines (the default), or a larger model at a provider, counted at its cost; not at the `high` confidentiality level. |
| `lane`, `confidentiality` | as everywhere | At `high`, the page is written on our own machine only (`rapide`). |

### 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; `media`, `documents` and `quality` may come with it. On a page shared with you for editing, the new page is yours and comes back to its owner, shared. `POST /v1/deck/estimate` `{"shape", "template?", "lane?", "quality?"}` tells the seconds and what the week is asked without launching; `GET /v1/deck/jobs` lists your presentations, then the ones shared with you; `DELETE /v1/deck/jobs/{id}` cancels one (counted if our model was already writing: `charged`) or puts a finished one in the bin (`POST /v1/creations/{creation_id}/restore` takes it back within 48 hours). Your rights: `GET /v1/deck/me` → `limits.shapes`, `limits.media_max`; `service.online` says whether our model is up, `service.fallback` who writes when it is not, `service.qualities` the models behind each quality.

### Present it live

```
curl -X POST https://api.infinitepeople.ai/v1/deck/jobs/$ID/live -H "Authorization: Bearer $KEY"
# → {"code": "MIA-482", "join_url": "https://infinicia.ai/j/MIA-482", "presenter_url": "…/v1/deck/live/MIA-482/present?key=…",
#    "qr_svg": "<svg…>", "count": 0, "expires_at": …}
```

A room for twelve hours on a finished page: open `presenter_url` on the screen (keep its key to yourself: it drives the room) and let the room scan `qr_svg` or type `join_url` on their phones. The phones follow slide by slide, react, ask questions (moderated), answer what the presenter opens: a quiz question (points for speed, a ranking), a poll, a rating, a word cloud — the components the page holds (`deck-quiz`, `deck-poll`, `deck-rate`, `deck-ask`, `deck-cloud`). The same call gives the open room back; `GET` reads it; `DELETE …/live` ends it. A page shared with you for viewing can be presented too, in a room of your own. The phones' side needs no key: `GET /v1/deck/live/{code}` (the page), `POST …/join {name}` → a token, `GET …/events` (server-sent events: a `state` snapshot, then `join`, `start`, `slide`, `open`, `answers`, `reveal`, `close`, `react`, `end`), `POST …/react`, `…/ask`, `…/answer`; the presenter drives with `POST …/control {key, action}`.

| code | |
|---|---|
| `422` `invalid_request` | an unknown shape, theme, template or quality; a medium that is not on our media host |
| `409` `not_a_finished_page` | the page to rework is not finished yet |
| `403` `share_view_only`, `not_owner` | the page is shared with you for viewing only; only the owner deletes |
| `404` `in_bin` | the page is in the bin (`creation_id`, `restorable_until` say how to take it back) |
| job `failed`, `page_refused` | no model could make an acceptable page; the week was given back — rephrase |
| job `failed`, `engine_unavailable` | no model answered (our node down and no provider allowed at this confidentiality level); try again in a minute |
| job `failed`, `source_missing` | the page or a document to work from is gone |

## 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 "file=@nicolas.m4a" -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 "file=@acted.m4a" -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/projects` lists 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` (multipart `file`, jpeg/png/webp, 8 MB at most) or `POST …/{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 a `job` object shaped like `GET /v1/video/jobs/{id}` (`result_url` is 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`, no `expires_at`); taken out, it lives its own retention again.

`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"`).
- A voice creation (a text said, a recording converted): `POST /v1/voice/projects/{id}/assets` `{"job_id"}`; `GET …/assets`; `DELETE …/assets/{aid}`.

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](https://api.infinitepeople.ai/v1/studio/openapi.json). This page in Markdown: [/api/index.md](/api/index.md). A summary for language models: [/llms.txt](/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`.

Videos are generated by MiniMax H3 and brought to 1080p by SwiftVR or SeedVR2, songs by ACE-Step 1.5, the segmentation by SAM 3, the enlargement by Philip Hofmann's Nomos upscalers, the voices by VoxCPM2 (OpenBMB) and their conversion by CosyVoice 3 (FunAudioLLM) on our machines; the direction help and the lyrics are written by Qwen on Alibaba Model Studio (OpenAI as the fallback), the painted first image and the picture edits in the cloud by gpt-image-2.5, the transcript of a voice by Whisper (OpenAI). Every request is moderated before it runs. The [terms of use](/#terms) apply to what you make with a key as much as in the app.

Base URL: `https://api.infinitepeople.ai/v1`. Authentication: `Authorization: Bearer <your key>` (a key made on https://infinicia.ai/#keys). OpenAPI: https://api.infinitepeople.ai/v1/studio/openapi.json. The HTML version of this page: https://infinicia.ai/api/ — this Markdown file is for readers and language models.
