adkixDevelopers
API v1 · OpenAPI 3.1 · MCP

Your AI makes the video,
you approve.

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.

Base URLhttps://adkix.ai/api/v1
Create a video
curl https://adkix.ai/api/v1/contents \
  -H "Authorization: Bearer $ADKIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "source": { "url": "https://maisonlumen.fr" } }'
Get started

#Get started in 3 minutes

  1. Create a key

    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
  2. 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.

    GET /me
    curl https://adkix.ai/api/v1/me \
      -H "Authorization: Bearer $ADKIX_API_KEY"
  3. Create a content

    A website, an Instagram @handle or an idea. adkix reads the brand right away; nothing paid starts. Keep the contentId.

    POST /contents
    curl https://adkix.ai/api/v1/contents \
      -H "Authorization: Bearer $ADKIX_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "source": { "url": "https://maisonlumen.fr" } }'
  4. Follow the state

    GET /contents/{id}?waitSec=25 waits until something happens. Read step, show the previews, and offer suggested: the next click adkix would make.

    GET /contents/{id}
    curl "https://adkix.ai/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19?waitSec=25" \
      -H "Authorization: Bearer $ADKIX_API_KEY"
  5. Act, get a quote

    Once the person agrees, run the action. If it costs credits, the answer is a quote: { quote: { proposalId, price, text } }. Nothing is spent.

    POST /contents/{id}/actions
    curl https://adkix.ai/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19/actions \
      -H "Authorization: Bearer $ADKIX_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "action": "approve_script", "args": {} }'
  6. Confirm after the yes, get the film

    Show quote.text, and only after an explicit yes, confirm. The shoot takes 15 to 20 minutes; at the end, video.shareUrl opens the film in any browser.

    POST /contents/{id}/confirm
    curl https://adkix.ai/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19/confirm \
      -H "Authorization: Bearer $ADKIX_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "proposalId": "p-7c41e9a02b6d" }'
Get started

#How it works

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.

13Shoottournage

Every scene filmed by the AI, 15 to 20 minutes.

quote
14Soundson

Music, sound effects.

15Reelreel

Edit, exports, subtitles.

1GETYour AI reads the state

step, previews, actions

2suggestedadkix proposes

never run by the server

3humanThe person approves

every step, every choice

4POSTYour AI acts

free: done · paid: quote

5yesConfirm

the only call that spends

start again, until the film

#Quotes

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.

#The shoot

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.
Get started

#Authentication

Every request carries the key in the Authorization: Bearer adk_live_… header. It is the only method: no cookie, no URL parameter.

  • At most 5 active keys per account, created and revoked on /compte. Revoking takes effect immediately.
  • Changing the password invalidates every key (and their signed links).
  • The key acts with the account's rights, teams included; never with admin rights.
  • The API answers no CORS request: call it from a server, a terminal or an agent, never from a web page.
  • Creating a key requires a confirmed email address.
Keep the key secret
It is shown only once. Store it in ADKIX_API_KEY or ~/.config/adkix/key (chmod 600). If it leaks, revoke it and create another one.
Header
Authorization: Bearer adk_live_ab12cd34_••••••••••••
401 Unauthorized
{
  "error": "clé invalide ou révoquée",
  "code": "auth"
}
Reference

#Endpoints

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 }.

#Sign up from the AI

POST/api/v1/auth/start
startSignInPublic, no key

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).
Parameters
  • emailstringbodyrequired

    The person's email address.

  • namestringbody

    Their first name, for a new account.

  • langfr | en | esbody

    Language of the email and of the account.

Request
curl -X POST "https://adkix.ai/api/v1/auth/start" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "name": "Camille",
    "lang": "fr"
  }'
200 OK
{
  "requestId": "Qm9uam91ci1hZGtpeC0yMDI2",
  "email": "[email protected]",
  "expiresInSec": 900
}

#Exchange the code for a key

POST/api/v1/auth/verify
verifySignInPublic, no key

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).

Request
curl -X POST "https://adkix.ai/api/v1/auth/verify" \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "Qm9uam91ci1hZGtpeC0yMDI2",
    "code": "482913"
  }'
201 Created
{
  "key": "adk_live_…",
  "created": true,
  "email": "[email protected]",
  "balance": 1000
}

#Read the account

GET/api/v1/me
getAccountBearer requiredread · 120 / min

The key's account: name, credit balance, plan, and whether the welcome video is still available. Call it first to check that the key works.

  • freeVideo is available, verify_email (address to confirm first), used or unavailable. available = createContent accepts offer: "free".
Parameters

No parameters.

Request
curl "https://adkix.ai/api/v1/me" \
  -H "Authorization: Bearer $ADKIX_API_KEY"
200 OK
{
  "id": "u_4d9a61f0",
  "name": "Camille Rey",
  "balance": 2400,
  "plan": "Studio",
  "freeVideo": "used"
}

#List contents

GET/api/v1/contents
listContentsBearer requiredread · 120 / min

The 50 most recent contents of the account and of the team projects shared with it, newest first.

  • thumbUrl is relative and needs the Bearer. step is the rail step, the same word as in the state.
Parameters

No parameters.

Request
curl "https://adkix.ai/api/v1/contents" \
  -H "Authorization: Bearer $ADKIX_API_KEY"
200 OK
{
  "contents": [
    {
      "id": "3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19",
      "projectId": "b71e04c2-9a3d-4f65-8c20-6e1f5d7a3b48",
      "title": "Maison Lumen",
      "status": "script_review",
      "step": "script",
      "createdAt": "2026-10-20T08:12:44.120Z",
      "thumbUrl": "/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19/files/frames/1",
      "videoReady": false
    }
  ]
}

#Create a content

POST/api/v1/contents
createContentBearer requiredcreate · 10 / day

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."
    }
  }'
201 Created
{
  "contentId": "3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19",
  "projectId": "b71e04c2-9a3d-4f65-8c20-6e1f5d7a3b48",
  "webUrl": "https://adkix.ai/projets/b71e04c2-9a3d-4f65-8c20-6e1f5d7a3b48?job=3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19",
  "state": {
    "id": "3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19",
    "status": "ingest",
    "step": "marque",
    "busy": {
      "label": "Lecture du site"
    },
    "suggested": null,
    "pollAfterSec": 10,
    "…": "…"
  }
}

#Read a content's state

GET/api/v1/contents/{id}
getContentBearer requiredread · 120 / min

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).

  • waitSecinteger 0–25query

    Long poll. Replaces sleeps between two calls.

Request
curl "https://adkix.ai/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19?waitSec=25" \
  -H "Authorization: Bearer $ADKIX_API_KEY"
200 OK · x-adkix-waited: changed
{
  "id": "3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19",
  "projectId": "b71e04c2-9a3d-4f65-8c20-6e1f5d7a3b48",
  "webUrl": "https://adkix.ai/projets/b71e04c2-9a3d-4f65-8c20-6e1f5d7a3b48?job=3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19",
  "title": "Maison Lumen",
  "status": "script_review",
  "step": "script",
  "rail": {
    "marque": "done",
    "elements": "done",
    "format": "done",
    "angle": "done",
    "medium": "done",
    "narration": "done",
    "options": "done",
    "style": "done",
    "casting": "done",
    "script": "active",
    "voix": "todo",
    "qualite": "todo",
    "tournage": "todo",
    "son": "todo",
    "reel": "todo"
  },
  "busy": null,
  "progress": {
    "pct": 48,
    "label": "Script à valider",
    "waiting": true
  },
  "error": null,
  "free": false,
  "balance": 2400,
  "context": {
    "brand": {
      "name": "Maison Lumen",
      "promise": "Des bougies coulées à la main à Lyon"
    },
    "format": {
      "platforms": [
        "instagram",
        "tiktok"
      ],
      "language": "fr",
      "duration": 30
    },
    "angle": "La bougie qu'on rallume chaque soir",
    "style": "Tournage, lumière chaude",
    "script": {
      "phrases": 9,
      "words": 78
    }
  },
  "actions": [
    {
      "name": "open_voice",
      "hint": "{} (the script is validated: on to picking and hearing the voice-over, free)"
    },
    {
      "name": "edit_phrase",
      "hint": "{ \"phraseId\": number, \"text\"?: string, \"highlight\"?: string }"
    },
    {
      "name": "rewrite_script",
      "hint": "{ \"note\": string }"
    }
  ],
  "suggested": {
    "action": "open_voice",
    "args": {},
    "price": 0
  },
  "previews": [
    {
      "kind": "frame",
      "label": "Plan 1",
      "url": "/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19/files/frames/1",
      "shareUrl": "https://adkix.ai/api/v1/links/eyJ1IjoiNGQ5YSIsImsiOiJrXzhmMmEiLCJqIjoiM2Y5YyJ9.Xk2vQ7tLwz…"
    }
  ],
  "video": null,
  "pendingQuote": null,
  "notice": null,
  "pollAfterSec": 30
}

#Run an action

POST/api/v1/contents/{id}/actions
runActionBearer requiredaction · 30 / min

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.

  • proposalIdstringbody

    For cancel (or confirm): the quote it targets.

Request
curl -X POST "https://adkix.ai/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19/actions" \
  -H "Authorization: Bearer $ADKIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "approve_script",
    "args": {}
  }'
200 OK
{
  "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
  }
}

A paid action answers with a quote. A free action answers { "ok": true, "note": "…" }.

#Accept a quote

POST/api/v1/contents/{id}/confirm
confirmQuoteBearer requiredaction · 30 / min

Accepts a quote and spends the credits. Call it only after the person's yes on that exact price. It is the only call that debits.

  • A quote lives 10 minutes. Expired or replaced: 409 proposalExpired / proposalSuperseded, ask for a new quote.
  • Balance too low: 402 notEnoughCredits with credits.need and credits.balance.
  • A dedicated route so a GPT can flag it « consequential »: ChatGPT always asks before calling it.
Parameters
  • idstringpathrequired

    The content's id (contentId from createContent, or id from listContents).

  • proposalIdstringbodyrequired

    quote.proposalId from runAction, or state.pendingQuote.proposalId.

Request
curl -X POST "https://adkix.ai/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19/confirm" \
  -H "Authorization: Bearer $ADKIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "proposalId": "p-7c41e9a02b6d"
  }'
200 OK
{
  "ok": true,
  "note": "Tournage lancé : 12 scènes, environ 15 à 20 minutes."
}

#Download a preview

GET/api/v1/contents/{id}/files/{kind}/{name}
getContentFileBearer requiredread · 120 / min

An image, a clip or a sound listed in state.previews (its url field). Needs the Bearer; for a human, give the shareUrl instead.

Parameters
  • idstringpathrequired

    The content's id (contentId from createContent, or id from listContents).

  • kindmoodboard | frames | videos | audiopathrequired

    The preview's family.

  • namestringpathrequired

    Last segment of the preview's url: 1, option-2, cast-a, phrase-3…

Request
curl "https://adkix.ai/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19/files/frames/1" \
  -H "Authorization: Bearer $ADKIX_API_KEY" \
  -o plan-1.png

The file: image/png, image/jpeg, video/mp4 or audio/mpeg.

#Download the film

GET/api/v1/contents/{id}/video
getContentVideoBearer requiredread · 120 / min

The finished film as MP4, to download or to stream. Range requests are accepted (206 answer).

Parameters
  • idstringpathrequired

    The content's id (contentId from createContent, or id from listContents).

  • aspect"16:9"query

    The horizontal version, when state.video.aspects has it.

  • download"1"query

    Adds Content-Disposition: attachment.

  • Rangebytes=…header

    Partial read, 206 answer.

Request
curl "https://adkix.ai/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19/video?download=1" \
  -H "Authorization: Bearer $ADKIX_API_KEY" \
  -o maison-lumen.mp4

video/mp4 (200, or 206 with Range).

#Download an export

GET/api/v1/contents/{id}/export/{kind}
getContentExportBearer requiredread · 120 / min

The finished film's exports: subtitles, cover, soundtrack, or a version without burned-in titles.

Parameters
  • idstringpathrequired

    The content's id (contentId from createContent, or id from listContents).

  • kindsrt | cover | audio | cleanpathrequired

    srt = subtitles, cover = cover image, audio = soundtrack, clean = film without titles.

  • aspect"16:9"query

    The export of the horizontal version.

Request
curl "https://adkix.ai/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19/export/srt" \
  -H "Authorization: Bearer $ADKIX_API_KEY" \
  -o maison-lumen.srt

The export's file.

GET/api/v1/links/{token}
openSignedLinkPublic, no key

The state's shareUrl and downloadUrl. They open in any browser, without a Bearer, for 24 h: that is what your AI gives the person.

  • Expired link, revoked key or changed password: 404 link_invalid. Read the state again for a fresh link.
Parameters
  • tokenstringpathrequired

    The token of a shareUrl / downloadUrl.

Request
curl "https://adkix.ai/api/v1/links/eyJ1IjoiNGQ5YSIsImsiOiJrXzhmMmEiLCJqIjoiM2Y5YyJ9.Xk2vQ7tLwz…" \
  -o film.mp4

The file, with Range for the film.

#OpenAPI spec

GET/api/v1/openapi.json
getOpenApiSpecPublic, no key

This API described in OpenAPI 3.1, public and readable from any origin: Postman, Swagger, SDK generators, or a GPT import.

  • ?profile=gpt gives a GPT's Actions profile: JSON operations only, confirmQuote flagged as consequential.
Parameters
  • profilefull | gptquery

    full by default.

Request
curl "https://adkix.ai/api/v1/openapi.json?profile=gpt"
200 OK
{
  "openapi": "3.1.0",
  "info": {
    "title": "adkix API",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://adkix.ai/api/v1"
    }
  ],
  "paths": {
    "/me": {},
    "/contents": {},
    "…": {}
  }
}
Reference

#The state object

GET /contents/{id} returns everything needed to decide what comes next. Read step and busy first, then actions and suggested.

FieldTypeDescription
idstringThe content's id.
projectIdstring | nullThe project (the brand) it belongs to.
webUrlstringThe content in the adkix studio: the person can pick everything up there.
titlestring | nullTitle (the site's name at first).
statusJobStatusbrief, ingest, moodboard, moodboard_review, script, script_review, frames, render, done, error.
stepStepThe rail step the content is at. This is what counts, not status.
railRecord<Step, State>Each step: todo, active, done or error. The order can vary with the Style flow: trust step.
busy{ label } | nullA task is running (« Lecture du site », « Tournage »…): wait with waitSec.
progress{ pct, label, waiting }0–100, never goes back; waiting = the person must approve.
error{ step, message } | nullA failed step. relaunch or retry_task restart it.
freebooleanWelcome video: quotes show a price but nothing is debited.
balancenumberCredits left on the account (or the team seat).
contextobjectWhat has been decided: brand, format, angle, style, casting, script, voice… Never a disk path or a provider URL.
actions{ name, hint }[]The actions allowed now, with the shape of their arguments.
suggested{ action, args, price } | nullThe next click adkix would make. Never run by the server: your AI offers it, the person decides.
previewsPreview[]Images, clips, voices, music to show: kind, label, url (Bearer) and shareUrl (24 h, no Bearer).
videoobject | nullThe finished film: ready, url, aspects, shareUrl, downloadUrl, linkExpiresInSec.
pendingQuoteQuote | nullA quote waiting for a yes (under 10 minutes), including one created from the site.
notice{ kind, text, at } | nullThe thread's last error message (a site that does not answer…), when error stays empty.
pollAfterSecnumber10 during a task, 30 otherwise: the advised pace without waitSec.
A content at the script step
{
  "id": "3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19",
  "projectId": "b71e04c2-9a3d-4f65-8c20-6e1f5d7a3b48",
  "webUrl": "https://adkix.ai/projets/b71e04c2-9a3d-4f65-8c20-6e1f5d7a3b48?job=3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19",
  "title": "Maison Lumen",
  "status": "script_review",
  "step": "script",
  "rail": {
    "marque": "done",
    "elements": "done",
    "format": "done",
    "angle": "done",
    "medium": "done",
    "narration": "done",
    "options": "done",
    "style": "done",
    "casting": "done",
    "script": "active",
    "voix": "todo",
    "qualite": "todo",
    "tournage": "todo",
    "son": "todo",
    "reel": "todo"
  },
  "busy": null,
  "progress": {
    "pct": 48,
    "label": "Script à valider",
    "waiting": true
  },
  "error": null,
  "free": false,
  "balance": 2400,
  "context": {
    "brand": {
      "name": "Maison Lumen",
      "promise": "Des bougies coulées à la main à Lyon"
    },
    "format": {
      "platforms": [
        "instagram",
        "tiktok"
      ],
      "language": "fr",
      "duration": 30
    },
    "angle": "La bougie qu'on rallume chaque soir",
    "style": "Tournage, lumière chaude",
    "script": {
      "phrases": 9,
      "words": 78
    }
  },
  "actions": [
    {
      "name": "open_voice",
      "hint": "{} (the script is validated: on to picking and hearing the voice-over, free)"
    },
    {
      "name": "edit_phrase",
      "hint": "{ \"phraseId\": number, \"text\"?: string, \"highlight\"?: string }"
    },
    {
      "name": "rewrite_script",
      "hint": "{ \"note\": string }"
    }
  ],
  "suggested": {
    "action": "open_voice",
    "args": {},
    "price": 0
  },
  "previews": [
    {
      "kind": "frame",
      "label": "Plan 1",
      "url": "/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19/files/frames/1",
      "shareUrl": "https://adkix.ai/api/v1/links/eyJ1IjoiNGQ5YSIsImsiOiJrXzhmMmEiLCJqIjoiM2Y5YyJ9.Xk2vQ7tLwz…"
    }
  ],
  "video": null,
  "pendingQuote": null,
  "notice": null,
  "pollAfterSec": 30
}
state.video, once the film is ready
{
  "video": {
    "ready": true,
    "url": "/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19/video",
    "aspects": [
      "9:16",
      "16:9"
    ],
    "shareUrl": "https://adkix.ai/api/v1/links/eyJ1IjoiNGQ5YSIsImsiOiJrXzhmMmEiLCJqIjoiM2Y5YyJ9.Xk2vQ7tLwz…",
    "downloadUrl": "https://adkix.ai/api/v1/links/eyJ1IjoiNGQ5YSIsImsiOiJrXzhmMmEiLCJqIjoiM2Y5YyJ9.Xk2vQ7tLwz…dl",
    "linkExpiresInSec": 86400
  }
}
Reference

#Actions

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).
  • edit_phrase
    { "phraseId": number, "text"?: string, "highlight"?: string }
  • rewrite_script
    { "note": string }
  • set_layout
    { "shotId": number, "layout": "kinetic"|"title"|"clean" }
  • set_scene_text
    { "shotId": number, "textMode"?: "phrase"|"keywords"|"none", "keywords"?: string }
  • set_transition
    { "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
  • relaunch
    {}
  • goto_step
    { "step": string }

#What stays on the site

Five actions answer 400 not_in_api: they need a file upload, or would spend without a quote. Send the person to webUrl.

ActionWhy
set_modeTurbo mode chains the paid steps without quotes.
edit_sceneRetouching a scene with uploaded images.
revert_scene_editUndoes a retouch made on the site.
set_cast_photoUploading a photo for a character.
derive_reelA 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",
    "…"
  ]
}
Reference

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.
One item of state.previews
{
  "kind": "frame",
  "label": "Plan 3",
  "url": "/api/v1/contents/3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19/files/frames/3",
  "shareUrl": "https://adkix.ai/api/v1/links/eyJ1IjoiNGQ5YSIsImsiOiJrXzhmMmEiLCJqIjoiM2Y5YyJ9.Xk2vQ7tLwz…"
}
Reference

#Wait without blocking

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.
Follow a shoot
while true; do
  STATE=$(curl -s "https://adkix.ai/api/v1/contents/$ID?waitSec=25" \
    -H "Authorization: Bearer $ADKIX_API_KEY")
  echo "$STATE" | jq -r '.progress.label, (.suggested.action // "—")'
  [ "$(echo "$STATE" | jq -r '.video.ready // false')" = "true" ] && break
done

#The recommended loop

A content from start to finish
const API = "https://adkix.ai/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.ADKIX_API_KEY}`,
  "Content-Type": "application/json",
};

async function call(method, path, body) {
  const res = await fetch(API + path, { method, headers, body: body && JSON.stringify(body) });
  if (res.status === 429) {
    await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after") ?? 10) * 1000));
    return call(method, path, body);
  }
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
  return data;
}

// 1. A content from a website
const { contentId } = await call("POST", "/contents", {
  source: { url: "https://maisonlumen.fr" },
  brief: { duration: 30, language: "fr" },
});

// 2. Follow it, step by step
while (true) {
  const s = await call("GET", `/contents/${contentId}?waitSec=25`);
  if (s.video?.ready) {
    console.log("Film ready:", s.video.shareUrl);
    break;
  }
  if (s.busy || !s.suggested) continue; // adkix is working: keep waiting

  // 3. Show s.previews and s.suggested to the person, get their decision
  const choice = await askPerson(s); // { action, args } or null
  if (!choice) continue;
  const r = await call("POST", `/contents/${contentId}/actions`, choice);

  // 4. A price? Nothing is spent without their yes
  if (r.quote && (await askPersonYes(r.quote.text))) {
    await call("POST", `/contents/${contentId}/confirm`, { proposalId: r.quote.proposalId });
  }
}
Reference

#Errors

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.

402 Payment Required
{
  "error": "Crédits insuffisants.",
  "code": "notEnoughCredits",
  "credits": {
    "need": 1180,
    "balance": 400
  }
}
CodeHTTPMeaningWhat to do
auth401Key missing, malformed, unknown or revoked.Check the Authorization: Bearer adk_live_… header; create a new key on /compte.
rate429Too many requests.Wait Retry-After seconds, then resume.
not_found404Unknown content or not yours, or no such route.Check the id with listContents.
method_not_allowed405Method not accepted by the route (Allow header).Use the documented method.
bad_action400Unknown action name or malformed body. validActions lists every name.Pick a name from state.actions.
not_in_api400Action reserved to the site (set_mode, edit_scene, revert_scene_edit, set_cast_photo, derive_reel).Send the person to webUrl.
bad_source400Neither source.url nor source.idea, or a free offer without a URL.Give a URL or an idea.
bad_brief400Invalid brief: duration not 15/30/60, unknown language, unknown platforms.Fix the field named in error.
free_refused403Welcome video refused; reason says why (already used, address to verify…).Create the content with offer: "paid".
notEnoughCredits402Balance too low; credits.need and credits.balance.The person tops up on adkix.ai, then asks for a new quote.
proposalExpired409The quote is more than 10 minutes old.Run the action again for a fresh quote.
proposalSuperseded409A newer quote replaced it.Use state.pendingQuote.
service_unavailable503A provider (image, video, voice) is down.Try again later; nothing was debited.
site_down / site_blocked400The site does not answer, or refuses robots (welcome video).Another URL, an @handle or an idea.
verify_email403Email address not confirmed yet.The person clicks the link they received by email.
bad_webhook400Webhook refused: URL not https, not public, or unknown event.A public https URL and events from the list.
too_many_webhooks409Already 5 webhooks on the account.Delete one with DELETE /webhooks/{id}.
link_invalid404Signed link expired, key revoked or password changed.Read the state again for a fresh link.
Reference

#Limits

20 / 10 minAuthentication failuresper IP address, then 429
120 / minReads (GET)per key
30 / minActions and confirmationsper key
10 / dayContent creationsper key, and 20 / day per account
10 minQuote lifetimeexpiresInSec = 600
24 hSigned linkslinkExpiresInSec = 86400
≤ 25 sLong pollwaitSec
5Active keysper account
50listContentsthe most recent

A limit that is exceeded answers 429 with the Retry-After header, in seconds.

Integrations

#Use adkix from Claude or ChatGPT

No code to write: give the key to your assistant, it drives the video with you.

#Claude: the adkix skill

An Agent Skill for Claude Code, Claude Desktop and claude.ai. With it, Claude:

  • understands your brand from your website, your @handle or a sentence;
  • starts the content, shows you every step (images, voices, clips) and waits for your approval;
  • never spends a credit without your yes on a precise price;
  • follows the 20-minute shoot without blocking the conversation, then gives you the link to the film.

Downloads: adkix-skill.zip (the full folder) or adkix.md (a single file).

  1. Claude Code: paste the install command, or unzip the folder into ~/.claude/skills.
  2. claude.ai and Claude Desktop: Settings, Capabilities, Skills, then upload the zip file.
  3. The key: in ~/.config/adkix/key (chmod 600) or the ADKIX_API_KEY variable. The skill never displays it.
Install in Claude Code
claude "Install this skill in ~/.claude/skills/adkix/SKILL.md: https://adkix.ai/connect/adkix.md"
Store the key (in your terminal)
mkdir -p ~/.config/adkix && (umask 077; printf 'Paste your adkix key, then Enter: '; \
  read -rs K; printf %s "$K" > ~/.config/adkix/key; chmod 600 ~/.config/adkix/key; echo; echo OK)
adkix-skill.zip
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

#ChatGPT: an adkix GPT

  1. In ChatGPT, Explore GPTs, Create, Configure tab.
  2. Paste the instructions from /connect/chatgpt/instructions.md.
  3. Actions, Create new action, Import from URL: https://adkix.ai/api/v1/openapi.json?profile=gpt.
  4. Authentication: API Key, Bearer type, paste your key.
  5. 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 and Cursor

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"
Integrations

#MCP server

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.
ToolRole
adkix_get_accountAccount, balance, welcome video (getAccount).
adkix_list_contentsThe 50 latest contents (listContents).
adkix_create_contentNew content from a website, an @handle or an idea (createContent).
adkix_get_contentThe state, with waitSec to wait for a change (getContent).
adkix_view_previewLooks 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_quoteAccepts a quote after the person's yes (confirmQuote). Annotated destructiveHint: the client asks for permission.
adkix_cancel_quoteWithdraws 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).
Install the server
claude mcp add --transport http adkix https://adkix.ai/api/mcp \
  --header "Authorization: Bearer adk_live_..."

Cursor: ~/.cursor/mcp.json. VS Code: .vscode/mcp.json. Codex: ~/.codex/config.toml, with the key in ADKIX_API_KEY.

Test by hand
curl https://adkix.ai/api/mcp \
  -H "Authorization: Bearer $ADKIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }'
Integrations

#Webhooksbeta, on request

Instead of watching the state, receive a POST when a content needs the person or when the film is ready.

POST/api/v1/webhooks
GET/api/v1/webhooks
DELETE/api/v1/webhooks/{id}
  • Create: { url, events? } → 201 { id, url, events, secret, createdAt }. The secret (whsec_…) is shown only once.
  • List: { webhooks: [{ id, url, events, createdAt, lastDelivery }] }. Delete: { ok: true }.
  • The last delivery: lastDelivery { at, event, http, ok, error? }.
  • At most 5 URLs per account (otherwise 409 too_many_webhooks), public https only (otherwise 400 bad_webhook). Without events, every event.
  • A webhook belongs to the key that created it: revoking the key stops its deliveries.
  • An event goes out about 3 seconds after the content settles, once per change; it fires again if the same step comes back with new content.
EventWhen
content.step_readyA step is waiting for the person (pick an angle, approve the script…).
content.quote_pendingA price is waiting for a yes; data.quote carries proposalId, action and price.
content.video_readyThe film is ready; data.video.shareUrl opens it.
content.failedA step failed.
POST /webhooks
curl https://adkix.ai/api/v1/webhooks \
  -H "Authorization: Bearer $ADKIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/adkix",
    "events": ["content.step_ready", "content.quote_pending", "content.video_ready", "content.failed"]
  }'
201 Created
{
  "id": "wh_5e2c19a07b",
  "url": "https://example.com/hooks/adkix",
  "events": [
    "content.step_ready",
    "content.quote_pending",
    "content.video_ready",
    "content.failed"
  ],
  "secret": "whsec_9Qm2…",
  "createdAt": "2026-10-20T09:30:12.004Z"
}
List, delete
curl https://adkix.ai/api/v1/webhooks \
  -H "Authorization: Bearer $ADKIX_API_KEY"

curl -X DELETE https://adkix.ai/api/v1/webhooks/wh_5e2c19a07b \
  -H "Authorization: Bearer $ADKIX_API_KEY"

#Verify the signature

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.

  1. Read the raw body, before any JSON parsing.
  2. Recompute the HMAC and compare it in constant time.
  3. Reject a t older than 5 minutes (replay).
  4. Ignore an Adkix-Event-Id already handled.

#Retries

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.

A delivery
POST /hooks/adkix HTTP/1.1
Content-Type: application/json
User-Agent: adkix-webhooks/1.0
Adkix-Event-Id: evt_01a9f3c47d2e
Adkix-Event-Type: content.video_ready
Adkix-Signature: t=1792488612,v1=5b0c7e1f9a…

{
  "id": "evt_01a9f3c47d2e",
  "type": "content.video_ready",
  "createdAt": "2026-10-20T09:50:12.000Z",
  "data": {
    "contentId": "3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19",
    "projectId": "b71e04c2-9a3d-4f65-8c20-6e1f5d7a3b48",
    "title": "Maison Lumen",
    "status": "done",
    "step": "reel",
    "webUrl": "https://adkix.ai/projets/b71e04c2-9a3d-4f65-8c20-6e1f5d7a3b48?job=3f9c2a7e-5b14-4d8a-9e61-0c7b2d4f8a19",
    "video": {
      "shareUrl": "https://adkix.ai/api/v1/links/eyJ1Ijoi…"
    }
  }
}
Receive and verify
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

function verifyAdkix(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(
    (header ?? "").split(",").map((p) => p.trim().split("=", 2)),
  );
  const t = Number(parts.t);
  if (!t || Date.now() / 1000 - t > toleranceSec) return false; // older than 5 min: rejected
  const want = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const got = Buffer.from(parts.v1 ?? "", "hex");
  return got.length === want.length && timingSafeEqual(got, want);
}

const app = express();
// the raw body, exactly as it was signed
app.post("/hooks/adkix", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyAdkix(raw, req.get("Adkix-Signature"), process.env.ADKIX_WEBHOOK_SECRET)) {
    return res.status(400).send("invalid signature");
  }
  const event = JSON.parse(raw);
  // Adkix-Event-Id: ignore an event already handled (a retry can happen)
  res.sendStatus(200); // any 2xx = delivered; answer fast, process afterwards
  handle(event);
});
Updates

#Changelog

OAuth sign-in, MCP registry
  • 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.
  • Quotes: nothing is spent without confirmation.
Updates

#Support

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.
  • Personal data: privacy policy. Terms: terms of use.