adkix turns a website into a short brand video, step by step. With the API, your AI is in the driver's seat: Claude, ChatGPT, Cursor or your own code. adkix proposes, the person approves every step and says yes to every price.
On adkix.ai/compte, API section, create a key. It starts with adk_live_ and is shown only once: keep it in an environment variable, never in your code.
Terminal
export ADKIX_API_KEY="adk_live_..."# the key, shown only once
Check the account
GET /me answers with your credit balance and the state of the welcome video. A 401 means the key is wrong or revoked.
A content is a video project. It moves along a rail of steps, from the brand to the edited film. Your AI drives, adkix proposes, and the person keeps control of every decision and every credit.
01Brandmarque
adkix reads the site: name, promise, colours, fonts.
02Elementselements
Products or places to show, a phrase that must be said.
03Formatformat
Platforms, language, duration.
04Angleangle
Three story ideas, the person picks one.
quote
05Stylemedium
Live action, UGC, motion design, cartoon, claymation…
06Narrationnarration
Voice-over, story, founder, dialogue…
07Optionsoptions
Titles, logo, call to action.
08Lookstyle
Four drawn moods, one kept.
09Castingcasting
Characters, places, objects, drawn.
10Scriptscript
The lines, scene by scene.
11Voice-overvoix
Picking the voice, reading every line.
12Qualityqualite
The video model, which sets the price of the shoot.
Every paid action first answers with a quote: proposalId, action, price in credits, text to read to the person, free (welcome video, nothing debited) and expiresInSec: 600.
Only POST /contents/{id}/confirm with that proposalId (or the confirm action) spends. An unconfirmed quote expires after 10 minutes; it also shows up in the site's thread and in state.pendingQuote.
Some free actions that undo work (changing the narration after the script, removing a character…) also answer with a 0-credit quote, with the warning in text.
Once approve_script is confirmed, the AI films every scene: allow 15 to 20 minutes for 30 seconds. Never block the conversation: follow progress with waitSec and keep the person posted.
Answer of a paid action
{
"quote": {
"proposalId": "p-7c41e9a02b6d",
"action": "approve_script",
"price": 1180,
"text": "Tourner coûte environ 1180 crédits, débités au coût réel au fil des étapes.",
"free": false,
"expiresInSec": 600
}
}
The golden rule
Your AI never confirms a quote without an explicit yes from the person on that exact price. The server never runs suggested on its own.
Every route lives under https://adkix.ai/api/v1, answers in JSON (except files) with Cache-Control: no-store, and an error always has the shape { error, code }.
No key yet: your AI asks for the person's email and sends it here. A 6-digit code goes out by email. Same path for a new account and an existing one; nothing is created before the code.
The code lives 15 minutes and works once. 3 emails per address per hour (429 beyond).
The person gives the code they received to your AI. The account is created if it does not exist (email verified, welcome credits), then an API key is returned, once.
5 wrong codes kill the request (401 expired: start /auth/start again). At most 5 active keys (409 too_many_keys).
Store the key in a private file, never in the conversation.
Parameters
requestIdstringbodyrequired
Returned by /auth/start.
codestringbodyrequired
The 6 digits from the email.
keyNamestringbody
The key's name on /compte (Claude Code by default).
Starts a video project from a website, an @instagram account or an idea, and reads the brand from the site. Nothing costly starts here: every paid step will answer with a quote later.
The brief starts nothing: it pre-fills state.suggested (format, language, duration, note for the angles).
offer: "free" uses the account's welcome video and requires source.url; a refusal answers free_refused with reason.
Limit: 10 creations per day per key, 20 per account.
Parameters
source.urlstring ≤ 500body
A website (https://…) or an @instagram account. source.url or source.idea is required.
source.ideastring ≤ 1000body
A free-text idea, without a website. It goes into the brief's note.
offer"paid" | "free"body
paid by default. free = the welcome video (source.url required).
igConsentbooleanbody
true when the person agrees that their public Instagram posts are read.
brief.duration15 | 30 | 60body
Duration in seconds (6, 12 or 24 scenes).
brief.languagestringbody
Voice-over language: fr, en, es, de, it, pt…
brief.platformsstring[]body
instagram, tiktok, shorts, youtube… Unknown ones are ignored.
brief.notestring ≤ 2000body
Free brief: offer, audience, tone, what must be said.
Request
curl-X POST "https://adkix.ai/api/v1/contents" \
-H"Authorization: Bearer $ADKIX_API_KEY" \
-H"Content-Type: application/json" \
-d '{
"source": {
"url": "https://maisonlumen.fr"
},
"brief": {
"duration": 30,
"language": "fr",
"platforms": [
"instagram",
"tiktok"
],
"note": "Lancement de la collection d'hiver, ton chaleureux."
}
}'
The source of truth: step, progress, allowed actions, adkix's suggestion, previews, film and pending quote. With waitSec, the answer waits for a change.
waitSec (0 to 25) holds the request open and answers as soon as the step, the running task, an image, a clip, a voice, the film or an error moves.
The x-adkix-waited header is changed, timeout or none (no wait asked).
Parameters
idstringpathrequired
The content's id (contentId from createContent, or id from listContents).
Runs an action of the current step (the names are in state.actions). A free action runs right away; a paid action spends nothing and answers with a quote.
Answer: either { ok, note } (done), or { quote } to show the person. Confirm only after an explicit yes.
action: "cancel" + proposalId withdraws a quote. action: "confirm" + proposalId is the same as confirmQuote.
An unknown name answers 400 bad_action with validActions; set_mode, edit_scene, revert_scene_edit, set_cast_photo and derive_reel answer 400 not_in_api.
Parameters
idstringpathrequired
The content's id (contentId from createContent, or id from listContents).
actionstringbodyrequired
A name from state.actions, or cancel.
argsobjectbody
Arguments, in the shape given by the action's hint.
The actions are the same as the site assistant's. Those allowed right now are in state.actions, with their hint: the exact shape of args. Main-path actions come first in each step.
main main pathestimate the quote is an estimate, credits are debited at the real cost as the steps goconfirmation may undo work: answers with a quote, even when free
Brand
marque7
brand_from_sitemain
{ "url": string }
keep_brandmain
{}
pick_brand
{ "brandId": string }
manual_brand
{ "name": string, "description"?: string, "promise"?: string, "audience"?: string } — fill what the user already said
set_brand_field
{ "field": "name" | "promise" | "description" | "audience" | "features" | "cta" | "url" | "language" | "colors" | "fonts" | "pronunciations", "value": unknown } — free, changes the brand itself (this reel's next pictures and every later reel); features is a string[]
sync_brand
{}
suggest_brand_copy
{}
Elements
elements3
finish_elementsmain
{ "skip"?: boolean }
set_reel_elementsconfirmation
{ "elementIds": string[], "mustInclude"?: string } (elements of the brand, at most 3; find them by name in the brand elements; mustInclude also sets the phrase in the same call)
set_must_includeconfirmation
{ "text": string } (what the reel must say word for word, ≤ 200 characters; "" removes it)
Format
format1
set_formatsmainconfirmation
{ "platforms"?: string[] (all vertical — tiktok/instagram/shorts — or only youtube, never both; left out = the format on screen), "language"?: "fr" | "en" | "es" | "de" | "it" | "pt", "duration"?: 15 | 30 | 60 (seconds: 6, 12 or 24 scenes) }
Angle
angle5
propose_anglesmain
{ "note"?: string }
pick_anglemain
{ "proposalId": string }
start_reelmainestimate
{}
retry_element_angles
{}
custom_angle
{ "text": string }
Style
medium2
choose_mediummain
{ "pack": number } (0-based, when packs are offered: medium + narration in one pick) or { "medium": string } — a catalogue id (tournage, ugc, motion-design, dessin-anime, anime, 3d-clay…), its French or English label, or any free wording (« en pâte à modeler ») taken as a custom medium. Free while the grid is shown; the next draw after « Changer de médium » costs the 4 styles.
change_medium
{} — back to the medium grid while the 4 styles wait; the styles already drawn stay until a new medium is chosen.
Narration
narration2
choose_narrationmain
{ "mode": string, "settings"?: object } — a catalogue id (voix-off, histoire, ugc, voix-interieure, fondateur, dialogue, interview) or its French / English label. "settings" (optional): { "titles": boolean (words painted on the pictures), "cta": boolean (the film ends on a call to action), "ctaText": string (its exact words, 60 characters at most; empty = written from the site) }. Free. The casting, the script and every plan are then made for that narration.
back_style_flow
{ "to": "medium" | "narration" } — Style flow: back to the Style (texture) or Narration screen before the ambiances are drawn. Free; the choices stay pre-checked.
Options
options1
set_optionsmain
{ "titles": boolean (words painted on the pictures), "logo": "never" | "end" | "corner", "cta": boolean (the film ends on a call to action), "ctaText": string (its exact words, 60 characters at most; empty = written from the site) } — Style flow screen 3 « Générer »: saves the options and draws the 4 ambiances (one in turbo). Prefer letting the person use the Options screen.
Look
style4
approve_stylemain
{ "index"?: number } (index = the style picked among the proposed ones, 0-based)
regenerate_moodboard
{ "note"?: string }
redo_typecard
{ "note"?: string }
redo_board
{ "index": 1 | 2 | 3 | 4 }
Casting
casting6
approve_castingmain
{} — keeps the drawn casting and writes the script with it
do_casting
{} — does the casting the server skipped on this content: the script is rewritten with it, the shots redrawn
set_cast_fieldconfirmation
{ "id": "<casting element id>", "fields": { … } } — free edit of the fiche, no drawing; keys: "name", character ["age","hair","outfit","accessories","signature"], place ["materials","light","never"]. e.g. « Joe s'appelle Gino » → { "id": "joe", "fields": { "name": "Gino" } }; "redraw": true also draws the element again from the edited fiche (one image, in the same run)
add_cast
{ "name": string, "kind": "character" | "place" | "object", "description": "the person's words" } — one new element, drawn once
remove_castconfirmation
{ "id": "<casting element id>" } — the element leaves the casting (free before the validation; after it, the scenes that showed it are redrawn)
redraw_cast_unitconfirmation
{ "unit": "character-<id>" | "place-<id>" | "objects", "note"?: "the person's instruction word for word (« brune, mate de peau »): the look is rewritten from it before drawing; without note the same look is drawn again" } — then offer recast_plans: the scenes keep the old look until their plans are redrawn — WITH A CASTING (the Casting card): { "id": "<casting element id>", "note"?: "the person's instruction word for word" } — e.g. « Refais Joe plus jeune » → { "id": "joe", "note": "plus jeune" }; without note the element is drawn again from its fiche. After the validation the scenes showing it are redrawn too.
Script
script16
approve_scriptmainestimate
{} (the SHOOT, paid: films every scene of the reel — only when the person asks to film / tourner / lancer le reel; validating the script or going to the voice is open_voice)
open_voicemain
{} (the script is validated: on to picking and hearing the voice-over, free)
change_narrationconfirmation
{ "mode": string, "settings"?: object } — same ids as choose_narration (the current id to change only its settings). "settings" (optional): { "titles": boolean (words painted on the pictures), "cta": boolean (the film ends on a call to action), "ctaText": string (its exact words, 60 characters at most; empty = written from the site) }. Only while the script is read (before the shoot): the script is written again for that narration and those settings, its voice and plans redone; the casting stays.
change_optionsconfirmation
{ "titles": boolean (words painted on the pictures), "logo": "never" | "end" | "corner", "cta": boolean (the film ends on a call to action), "ctaText": string (its exact words, 60 characters at most; empty = written from the site) } — Style flow, while the script is read (before the shoot): the logo end/corner is saved as it is (free); titles, the call to action or the logo to/from "never" rewrite the script (its voice and plans redone).
{ "shotId": number, "kind": "whip"|"through"|"match-action"|"match-shape"|"pass-lens"|"focus"|"crane", "prompt"?: "one English sentence naming the concrete element (the object the camera dives into, the gesture that carries on)" } (free: how the camera passes from this scene to the next, filmed in the clip; « fais un filé entre la 3 et la 4 » = shotId 3, kind whip; then offer redo_clip when that clip is already filmed)
sketch_scenes
{ "shotIds"?: number[] } (the scenes whose plan is missing or out of date; all of them when absent)
set_pronunciations
{ "list": [{ "word": string (one word, as written), "say": string (how the voice must say it) }] } (saved on the brand; the phrases it touches are read again, free)
set_scene_pronunciation
{ "phraseId": number, "word": string (one word of that phrase, as written), "say": string (how to say it; "" removes it) } (that scene only, re-read free; for every scene use set_pronunciations)
toggle_sfx
{ "on": boolean }
redo_sfx
{ "note"?: string }
remove_sfx_cue
{ "cueId": string ("sfx-N") } (before the shoot; after it, the montage removes a cue)
recast_plans
{}
Voice-over
voix3
open_qualitymain
{} (the voice is ready: on to choosing the video quality, free; only for a content that has the quality step)
pick_voicemain
{ "voiceId": string, "ownerId"?: string (library voice), "name"?: string, "speaker"?: "a" | "b" (a two-voice script: whose voice, default a), "read"?: boolean (false = only select it, make_voice reads it later; default true) } (then the voice-over is read with it, free; on a finished reel it is re-cut on the new voice, free)
make_voice
{} (reads the missing phrases again with the current voice, e.g. after a phrase edit; free)
Quality
qualite1
choose_qualitymain
{ "id": string } — the video quality, free: h3-768, h3-2k, sd2-720, sd2-1080 or sd25-1080 when nobody speaks on camera; omni-1080 when someone does. The price of the shoot follows it.
Shoot
tournage7
redo_scene
{ "shotId": number (1 to the scene count), "note"?: string, "refs"?: ["c_…" ids from context.media, 3 at most — the pictures the scene must be drawn from; [] takes them off; leave it out to keep the ones the scene has] }
redo_scenes
{ "scenes": [{ "shotId": number, "note"?: string, "refs"?: ["c_…" ids from context.media, 3 at most — the pictures the scene must be drawn from; [] takes them off; leave it out to keep the ones the scene has] }] } — several scenes redone at once, one price
redo_clip
{ "shotId": number (1 to the scene count) }
retry_task
{ "taskId": string } — a failed task from context.tasks, tried again
redo_clips
{}
add_landscape
{}
refilm_missing
{}
Sound
son5
pick_musicmain
{ "candidateId": string }
set_voice
{ "voiceId": string }
new_music_candidates
{}
rewrite_music_briefs
{ "note"?: string }
set_caption_style
{ "style": "karaoke"|"pop"|"clean"|"none", "layout"?: { "font": "inter"|"inter-heavy"|"inter-light"|null, "scale": 0.6-1.6, "y": 15-95 (bas du texte, % depuis le haut)|null } }
Reel
reel6
open_exportmain
{} (the montage is fine: on to the export step — downloads, captions, other language or format; free)
edit_montage
{ "phrasePadSec"?: { [phraseId]: seconds -0.3..1.5 | null }, "musicGainOffsetDb"?: -12..6 | null, "effectsGainOffsetDb"?: -20..6 | null (every sound effect and the sound of the clips at once), "captions"?: { [phraseId]: { "text"?: same word count, "highlight"?: word } | null }, "sfx"?: { [cueId]: { "anchor"?: "in"|"mid"|"out", "offsetSec"?: -0.5..0.5, "gainDb"?: -14..0, "removed"?: boolean } | null }, "cuts"?: { [shotId]: { "splits"?: seconds from the scene start[], "removed"?: [from, to][] (pieces taken out of the reel) } | null }, "wholeClips"?: true | false | null (true: every clip whole at its own speed, nothing cut, only the voice laid on the titles, the reel may pass 30 s; false: clips fitted to the voice; null: the reel default, context.wholeClipsDefault) }
export_clean
{}
write_post
{ "note"?: string }
open_montage
{} (back from the export to the montage step, to adjust scene lengths, music, captions or effects; free)
rerender
{}
At every step
3
forget_constraint
{ "id": string } — a constraint id from context.constraints
Five actions answer 400 not_in_api: they need a file upload, or would spend without a quote. Send the person to webUrl.
Action
Why
set_mode
Turbo mode chains the paid steps without quotes.
edit_scene
Retouching a scene with uploaded images.
revert_scene_edit
Undoes a retouch made on the site.
set_cast_photo
Uploading a photo for a character.
derive_reel
A variation of a content, planned for v2.
400 bad_action
{
"error": "Action inconnue : les noms valides sont dans « validActions » ; celles de l'étape en cours sont dans GET /api/v1/contents/:id (« actions »).",
"code": "bad_action",
"validActions": [
"add_cast",
"add_landscape",
"approve_casting",
"…"
]
}
An AI without a terminal cannot hand the person a URL that needs a Bearer header. So every preview carries a shareUrl, and the film a shareUrl and a downloadUrl.
They open in any browser, without a key, for 24 hours (linkExpiresInSec: 86400).
Each one opens a single file of a single content, and holds neither the key nor a session.
They are signed by the key that read the state: revoking the key or changing the password cuts them off at once.
Expired: 404 link_invalid. Read the state again to get fresh ones.
The film accepts Range requests: it can be played and scrubbed directly.
Reading a brand takes a few seconds, drawing styles one to two minutes, shooting 15 to 20 minutes. Instead of calling in a loop, ask the API to wait for you.
?waitSec=25 holds the request for up to 25 seconds and answers as soon as something changes: step, task, image, clip, voice, film, error.
The x-adkix-waited header says changed or timeout. Either way, just make the next call.
Without waitSec, follow pollAfterSec: 10 seconds when busy is set, 30 otherwise.
A 25 s request fits within the 45 s of a ChatGPT Action and within an MCP tool's timeout.
During the shoot, keep the person posted (progress.label, progress.pct) instead of making them wait in silence.
An error always has a JSON body { error, code }: error is a sentence you can show (written in French for now), code is stable and meant for your code. Some codes add a field.
adkix/
├── SKILL.md # when to use it, the loop, the pricing rules
├── reference/*.md # steps, actions, errors, API details
└── scripts/adkix.py # small client: reads the key without ever printing it
Actions, Create new action, Import from URL: https://adkix.ai/api/v1/openapi.json?profile=gpt.
Authentication: API Key, Bearer type, paste your key.
Save the GPT as « Only me »: it acts with your key.
The gpt profile keeps only the JSON operations. confirmQuote is flagged as consequential there: ChatGPT always asks you before spending. Images and the film open through their signed links. The full walkthrough is in /connect/chatgpt/README.md.
Import from URL
https://adkix.ai/api/v1/openapi.json?profile=gpt
Web connectors
The claude.ai and ChatGPT web connectors require OAuth, not available yet. Use the skill or the GPT in the meantime.
Codex discovers skills stored in ~/.codex/skills/<name>/SKILL.md: the same install command works. Cursor, Codex and VS Code can also talk to adkix through the MCP server.
Install in Codex
codex"Install this skill in ~/.codex/skills/adkix/SKILL.md: https://adkix.ai/connect/adkix.md"
A stateless Model Context Protocol server over Streamable HTTP, at https://adkix.ai/api/mcp. Two ways in: OAuth sign-in (your assistant opens an adkix page, you accept, nothing to copy) or your key in the Authorization header.
Protocol versions: 2025-11-25, 2025-06-18 and 2025-03-26. POST only (GET answers 405).
The server sends its instructions at initialization: your AI knows the pricing rules from the start.
A web page from an origin other than adkix.ai gets a 403.
Same limits and same quotes as the REST API.
Tool
Role
adkix_get_account
Account, balance, welcome video (getAccount).
adkix_list_contents
The 50 latest contents (listContents).
adkix_create_content
New content from a website, an @handle or an idea (createContent).
adkix_get_content
The state, with waitSec to wait for a change (getContent).
adkix_view_preview
Looks at a preview (file: frames/3, moodboard/option-2): returned as an MCP image (JPEG, 1024 px max), so your AI can describe it.
adkix_confirm_quote
Accepts a quote after the person's yes (confirmQuote). Annotated destructiveHint: the client asks for permission.
adkix_cancel_quote
Withdraws a quote (runAction cancel).
adkix_<action>
One tool per action (adkix_pick_voice, adkix_shoot_video…), with its arguments and annotations; only those in state.actions work right now, paid ones return a quote (runAction).
Every delivery (User-Agent adkix-webhooks/1.0) carries Adkix-Event-Id, Adkix-Event-Type and Adkix-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" with your secret.
Read the raw body, before any JSON parsing.
Recompute the HMAC and compare it in constant time.
3 attempts: right away, after 1 minute, after 10 minutes. 10-second timeout per attempt; any 2xx answer counts as delivered, and redirects are not followed. Answer fast and process afterwards.
The MCP server accepts OAuth 2.1 (PKCE S256, dynamic registration): Claude, ChatGPT or Codex connect with just the address https://adkix.ai/api/mcp, without copying a key.
adkix is listed in the official MCP registry as ai.adkix/adkix.
Signed links, long poll, OpenAPI
24 h signed links (shareUrl, downloadUrl) on every preview and on the film.
waitSec on getContent (up to 25 s) and the x-adkix-waited header.
POST /contents/{id}/confirm route to accept a quote.
Public OpenAPI 3.1 spec, gpt profile for Actions.
MCP server and webhooks in beta, on request.
Claude skill v3 (full folder) and GPT kit for ChatGPT.
API v1
Personal keys adk_live_…
Contents: create, list, read the state.
Actions of every step, same names as the site assistant.
A question, a bug, an integration that gets stuck: write to [email protected]. We answer in English, French or Spanish, usually within 24 business hours.
Give the content id (contentId) and the time of the call: we find the log right away.
Your assistants' connections (Claude, ChatGPT…) can be seen and cut off in « Connected apps », on your account.