Référence de l’API REST
L’API Hubfluencer.
Tout ce dont un agent — ou votre propre code — a besoin pour importer de vrais enregistrements d'app, organiser une timeline Editor, ajouter des ingrédients facultatifs et exporter un MP4 final. L'URL de base est hubfluencer.com, tous les chemins se trouvent sous /api et l'authentification utilise un token Bearer.
Vue d’ensemble & authentification
Envoyez chaque requête protégée avec un en-tête Authorization. Le jeton est opaque (un jeton de session ou un jeton d’accès personnel) — ce n’est pas un JWT, ne le décodez donc pas.
authorization: Bearer <token> content-type: application/json
Les scopes limitent ce qu’un jeton peut faire : video:read pour les lectures, video:generate pour tout ce qui est prélevé sur le solde du compte, account:admin pour la gestion du compte et des jetons. Les jetons d’agent n’obtiennent que video:generate + video:read.
Vous préférez une doc que votre agent peut lire directement ? Téléchargez la version Markdown de cette page : API.md . Une spec OpenAPI lisible par machine est aussi disponible sur /api/openapi (protégée par Bearer en production).
Enveloppe de réponse
// Success — usually wrapped in `data` (a few endpoints return the object at top level)
{ "data": { "...": "..." }, "error": null, "meta": { "timestamp": "2026-06-03T10:00:00Z" } }
// Error — shapes vary; parse defensively
{ "error": "balance_insufficient", "message": "Your balance is too low.",
"required": { "amount_minor": 160, "currency": "EUR" }, "available": { "amount_minor": 100, "currency": "EUR" } }
{ "errors": { "product_prompt": ["should be at least 10 character(s)"] } }
Jetons d'accès
Les jetons d’accès personnels (Personal Access Tokens) sont l’identifiant recommandé pour les agents. La chaîne du jeton n’est renvoyée qu’une seule fois, à la création. Gérer les jetons nécessite account:admin — un jeton d’agent ne peut donc ni créer ni révoquer de jetons ; créez-en un en étant connecté à l’app (Settings → Access tokens), ou lancez npx -y @hubfluencer/mcp@1.0.0 login.
/api/tokens
account:admin
Créer un jeton d’accès personnel (Personal Access Token).
| Champ du corps | Type | Remarques |
|---|---|---|
name optionnel |
string | Libellé affiché dans l’app. |
scopes optionnel |
string[] | Par défaut : video:generate + video:read. |
Requête
curl -X POST https://hubfluencer.com/api/tokens \
-H "authorization: Bearer $SESSION_TOKEN" \
-H "content-type: application/json" \
-d '{"name":"Claude Code","scopes":["video:generate","video:read"]}'
Réponse
201 Created
{
"data": {
"id": 42,
"name": "Claude Code",
"scopes": ["video:generate", "video:read"],
"token": "hf_pat_9f3c…ONCE_ONLY",
"inserted_at": "2026-06-03T10:00:00Z"
}
}
/api/tokens
account:admin
Lister vos jetons. Un scope ["*"] désigne un jeton à accès complet.
200 OK
{
"data": [
{ "id": 42, "name": "Claude Code",
"scopes": ["video:generate","video:read"],
"last_used_at": "2026-06-03T10:05:00Z",
"inserted_at": "2026-06-03T10:00:00Z" }
]
}
/api/tokens/:id
account:admin
Révoquer un jeton. Renvoie 204 No Content.
Solde & voix
Le travail payant est prélevé sur le solde du compte, tenu dans une seule devise par compte : EUR, USD ou GBP. La grille tarifaire affiche le même montant dans chaque devise (un short coûte 1,60) : les prix de cette page sont donc exprimés en unités principales de la devise de votre compte. Chaque montant d’une réponse est un objet monétaire {amount_minor, currency} — amount_minor est un nombre entier de centimes. Les plafonds de dépense (max_cost) sont de simples entiers en unités mineures de la devise du compte, jamais des flottants ni un code de devise : max_cost 115 approuve 1,15. Une requête qui envoie encore max_credits ou max_generation_credits est refusée avec 409 client_upgrade_required et rien n’est facturé.
/api/balance
video:read
Le solde du compte dans sa devise : balance, spendable (ce que les nouveaux travaux peuvent utiliser — comparez-y les devis), reserved et remaining_reserved (montants réservés pour un projet de première vidéo déjà payé), currency_locked (true dès que la devise du compte est fixée : la création du premier paiement de recharge du compte la fixe, même si ce paiement est ensuite annulé, et elle ne change plus jamais ensuite), ainsi que l’activité récente. Consultez-le avant de générer, annoncez le prix dans la devise du compte et obtenez le feu vert de l’utilisateur avant tout appel facturé. Les fonds sont ajoutés par le titulaire du compte dans l’app Hubfluencer ou dans Studio ; un agent ne recharge jamais le solde. Les clients peuvent envoyer un en-tête x-hubfluencer-currency (EUR, USD ou GBP) pour choisir la devise dans laquelle un compte est tarifé tant que sa devise n’est pas fixée ; le serveur l’ignore dès que currency_locked vaut true.
200 OK — every amount is integer minor units (cents) of the account currency
{
"data": {
"currency": "EUR", "currency_locked": true,
"balance": { "amount_minor": 1840, "currency": "EUR" },
"spendable": { "amount_minor": 1840, "currency": "EUR" },
"reserved": { "amount_minor": 0, "currency": "EUR" },
"remaining_reserved": { "amount_minor": 0, "currency": "EUR" },
"latest_purchase_activity_id": 991,
"account_reference": "c***@example.com",
"recent_activity": [
{ "id": 992, "kind": "scene",
"amount": { "amount_minor": -240, "currency": "EUR" },
"balance_after": { "amount_minor": 1840, "currency": "EUR" },
"inserted_at": "2026-09-23T10:05:00Z" },
{ "id": 991, "kind": "purchase",
"amount": { "amount_minor": 2000, "currency": "EUR" },
"balance_after": { "amount_minor": 2080, "currency": "EUR" },
"inserted_at": "2026-09-23T10:00:00Z" }
]
}
}
/api/voices
video:read
Set narration_script and language together with PATCH /api/editor/:slug before generating speech. Language is frozen into new voice requests and manual scripts use provider word timing. Narration voices. Pass ?language=<the project's language> for that language's catalog: recommended voices (source "featured", the language's default first) then popular ElevenLabs Voice Library voices in it (source "library"), each previewing in that language, plus default_voice_id. Without language, the curated list, each voice with verified_languages. Pass the chosen voice_id to generate-voice: a voice these lists offer for the project's language, a language default or one the account already narrated with (any other id is 422 invalid_voice_id; 503 voice_check_unavailable means retry shortly). Shorts have no voice-over.
GET /api/voices?language=fr
200 OK
{
"data": {
"voices": [
{ "voice_id": "KSyQzmsYhFbuOhqj1Xxv", "name": "Lea - UGC creator",
"source": "featured",
"labels": { "gender": "female", "use_case": "social_media", "language": "fr" },
"preview_url": "https://…/lea-fr.mp3" },
{ "voice_id": "ecxPjiGTvAfpGEams6ec", "name": "Paul K — French Ad & Trailer Voice",
"source": "library",
"labels": { "gender": "male", "use_case": "advertisement", "locale": "fr-FR" },
"preview_url": "https://…/paul-k-fr.mp3" }
],
"default_voice_id": "KSyQzmsYhFbuOhqj1Xxv",
"has_more": false,
"total_count": 2,
"next_page_token": null
}
}
Runs de génération & reprise
Les réponses produit Editor et Short sont la surface de lecture normale. Ces endpoints inspectent un run autorisé ou exécutent exactement une action needs-input déjà proposée par generation_summary.available_actions. N’inventez jamais d’action de reprise à partir des champs de projection produit. Relisez le produit juste avant une commande et renvoyez son run_id, sa revision et le step_key de l’action ; une barrière périmée échoue sans toucher au travail plus récent.
/api/generation-runs/:run_id
video:read
Inspecter un run géré par le serveur à partir de son UUID. Renvoie {data:{generation_summary}} avec le même schéma canonique que celui intégré aux lectures produit. La propriété est vérifiée après l’autorisation du scope ; un run inconnu ou appartenant à un autre compte renvoie 404 : un UUID ne confère donc jamais, à lui seul, de droit d’accès.
Les quatre commandes de reprise exigent video:generate et une Idempotency-Key. Ne réutilisez une clé que pour les relances réseau du même corps protégé par ses barrières ; toute nouvelle commande délibérée d’approbation ou d’input reçoit une nouvelle clé. L’objet input ne doit pas être vide. Une approbation de plafond de dépense est en ajout seul et ne réécrit pas la politique figée du run. En cas de succès, la réponse est {data:{generation_summary}} ; relisez-la avant l’action suivante. Une clé manquante renvoie 422 idempotency_key_required.
Un run en attente refuse les requêtes, il ne les met pas en file. Tant qu’un run est en attente sur needs_input, toute autre écriture sur ce projet — generate/regenerate/finalize/variants de scène, batch-generate, render et les modifications de structure de la timeline — est refusée avec 409 editor_generation_input_required au lieu d’un code transitoire d’occupation. En attente signifie que rien ne s’exécute : seule votre réponse le débloque, donc relancer cette requête ne peut jamais réussir. Les details du refus reprennent le blocage que generation_summary.blockers affiche déjà (blocker_code, step_key, segment_id, position, warning_codes) plus generation_run_id et generation_revision, soit exactement ce dont la commande de reprise ci-dessus a besoin — input {"continue_with_take": true} accepte une prise vérifiée, input {"segment_id": <id de la prise choisie>} choisit une variante. Les codes transitoires de propriétaire (editor_segment_busy, batch_generation_active, autopilot_active, voice_generation_busy, music_generation_busy, editor_render_in_progress) gardent leur sens habituel : du travail est réellement en cours, pollez et relancez.
POST /api/editor/:slug/generation/input
Idempotency-Key: recovery:<stable-command-id>
{"generation_run_id":"<uuid>","generation_revision":7,"step_key":"brief","input":{"product_name":"Acme"}}
Shorts
Un short est une pub verticale à prompt unique, composée de deux clips enchaînés. Trois requêtes : créer un brouillon (gratuit), lancer la génération (1,60), puis suivre le generation_summary obligatoire jusqu’à ce qu’il ne propose plus de cadence de polling. Une génération peut s’interrompre à une étape de vérification visuelle entre les clips ; n’invoquez que l’action de reprise proposée pour continuer. Envoyez une Idempotency-Key sur generate pour qu’une relance ne facture pas deux fois.
Chaque lecture produit Editor et Short inclut un generation_summary non nul. Traitez-le comme la seule autorité sur le cycle de vie, même quand stage, autopilot_status, batch_generation_status, les statuts enfants ou latest_render divergent. Ne pollez que tant que generation_summary.timing.poll_after_ms est un entier positif, exactement à cette cadence du serveur ; null signifie qu’il faut arrêter de poller. Lisez l’état terminal dans generation_summary.status, n’invoquez que generation_summary.available_actions et ne téléchargez que lorsque download_result est proposé pour un artefact de livraison vérifié. Les champs de projection produit ne sont que des métadonnées d’affichage ou des barrières de route explicites ; n’en déduisez jamais la disponibilité du polling, de la relance, de l’annulation ou du téléchargement.
/api/shorts
free
Créer un brouillon de short.
| Champ du corps | Type | Remarques |
|---|---|---|
product_prompt requis
|
string | ≥ 10 caractères — le sujet de la pub. |
language optionnel |
string | Ensemble fermé de codes ISO-639-1 simples ("en" par défaut) : en, fr, es, de, it, nl, pl, pt, ru, zh, ja, ko, ar. Les sous-étiquettes de région ou d’écriture comme "en-US" ou "pt-BR" sont refusées (422). |
headline optionnel |
string | TITRE incrusté à l’écran (plafond strict ≤80 ; ≤40 caractères ≈ 4 mots est l’idéal au rendu). Laissez headline, subheadline ET text_beats vides pour que generate rédige le texte à votre place (gratuit). |
subheadline optionnel |
string | Titre SECONDAIRE / ligne d’accompagnement (≤200). |
theme optionnel |
string | Obsolète pour les shorts : repli historique utilisé seulement quand visual_language n’est pas défini. Utilisez visual_language pour les nouveaux shorts. |
creative_format optionnel
|
string | Structure optionnelle : problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal. Omettez-la pour Auto. |
visual_language optionnel
|
string | Direction visuelle + rendu : kinetic_creator, premium_editorial, cinematic_product, ugc_realism, startup_explainer, luxury_minimal. Une création sans rien de tout cela (ni visual_language, ni theme) prend kinetic_creator par défaut ; envoyez un null explicite pour un rendu neutre simple. |
music_vibe optionnel |
string | Upbeat (par défaut), Cinematic, Minimal, Luxury, Playful, Jazz. Si rien n’est défini, la bande-son suit visual_language (p. ex. luxury_minimal → Luxury, cinematic_product → Cinematic). |
short_text_position, short_text_animation, short_font_family,
music_instruments
optionnel
|
string / array | Position de l’incrustation (top/center/bottom). Animation : auto (par défaut) / reveal / typewriter / fade_in / pop / bounce / word_stagger / word_spotlight. Police : auto (par défaut) ou l’une des 15 polices incluses — ShortFontSpaceGrotesk, ShortFontMontserrat, ShortFontTheBold, ShortFontImpact, ShortFontLato, ShortFontAnton, ShortFontBebasNeue, ShortFontOswald, ShortFontArchivoBlack, ShortFontPoppins, ShortFontInter, ShortFontTikTokSans, ShortFontBangers, ShortFontDMSerif, ShortFontPermanentMarker. "auto" suit la typographie de l’ambiance visual_language ; toute valeur concrète — police ou animation, y compris ShortFontSpaceGrotesk / fade_in — est un choix EXPLICITE qui l’emporte toujours. music_instruments : jusqu’à 10 indications, chacune ≤ 40 caractères. |
headline_color, subheadline_color, accent_color,
text_beats
optionnel
|
string / array | Couleurs d’incrustation (headline/subheadline acceptent l’hexadécimal #rgb–#rrggbbaa ; accent_color est strictement en #rrggbb à 6 chiffres), et text_beats — jusqu’à 8 lignes de légende (≤120 caractères chacune) affichées une à une à la place du sous-titre statique. En PATCH, envoyez [] pour effacer les lignes. |
offer_text, cta_text, badge_text,
star_rating
optionnel
|
string / number | Éléments de conversion : pastille d’offre (≤16), bouton CTA (≤24), tampon de badge (≤24) et une note de 0 à 5 étoiles. Offre, badge et étoiles sont strictement fournis par l’utilisateur et DÉSACTIVÉS sauf s’ils sont définis — n’affirmez que ce que vous pouvez prouver : badge_text et star_rating s’affichent comme des recommandations, envoyez donc une vraie note ou un vrai badge que vous détenez, jamais une valeur inventée. cta_text est la seule exception : votre valeur l’emporte toujours, mais si elle est vide, le rendu se rabat sur le default_cta de votre profil de marque, puis sur un préréglage localisé fixe sans allégation ("Shop now" pour les formats produit, "Learn more" sinon) — une invitation neutre localisée est TOUJOURS affichée ; un short vraiment sans CTA n’est pas pris en charge pour l’instant. Relisez la valeur résolue dans resolved_cta_text. En PATCH, envoyez "" (ou 0 pour star_rating) pour en retirer un. |
closing_claim, brand_name, end_card,
poster_includes_lockup
optionnel
|
string / boolean | Réglages de la carte de fin. Par défaut, chaque short se termine sur une carte de fin de 2 s (14 s au total) : votre affiche ou — si aucune n’est définie — un carton de lockup de marque généré (brand_name en grand, closing_claim ou le titre, bouton CTA). closing_claim (≤80) : une reformulation de la promesse pour que la carte de fin ne recycle pas l’accroche ; l’assist de rédaction peut le remplir. brand_name (≤40) : nom affiché (se rabat sur le nom de votre profil de marque). end_card accepte en écriture auto|none|lockup|poster (auto équivaut à lockup, ou à poster si une affiche est définie) et est stocké/renvoyé comme none|lockup|poster. none est l’option de sortie de 12 s, façon boucle. poster_includes_lockup (booléen, également accepté sur poster/confirm) n’est valide qu’avec poster : mettez-le à true quand votre affiche porte déjà son propre nom/CTA, afin de masquer l’incrustation. La même valeur à trois états est renvoyée dans end_card_mode. |
brand_profile_id optionnel
|
integer | Id de l’un de vos profils de marque (GET /api/brand-profiles). À la création, les valeurs par défaut de la marque (format créatif, langage visuel, police, CTA, couleur d’accent) remplissent les champs vides du short et un instantané est figé sur le short — le moyen le plus rapide d’habiller un short aux couleurs de la marque. Un id inconnu ou appartenant à un autre compte → 422 invalid_brand_profile. |
Requête
curl -X POST https://hubfluencer.com/api/shorts \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-d '{"product_prompt":"a 15s ad for my soy candle brand","language":"en","creative_format":"proof_demo","visual_language":"kinetic_creator"}'
Réponse
201 Created
{
"data": {
"slug": "amber-candle-9x2",
"stage": "draft",
"generation_summary": {
"run_id": null, "workflow": null, "workflow_version": null, "revision": null,
"status": "idle", "settling": false, "phase": "idle", "label": "Ready to generate",
"progress": {"completed": 0, "total": 0, "percent": 0}, "current_step": null,
"blockers": [],
"available_actions": [{"type":"start_generation","label":"Start generation","destructive":false,"step_key":null}],
"failure": null,
"cost": {"quoted":{"amount_minor":0,"currency":"EUR"},"charged":{"amount_minor":0,"currency":"EUR"},"refunded":{"amount_minor":0,"currency":"EUR"},"net":{"amount_minor":0,"currency":"EUR"},"max":{"amount_minor":0,"currency":"EUR"}},
"timing": {"started_at":null,"updated_at":null,"deadline_at":null,"poll_after_ms":null}
},
"language": "en",
"latest_render": null,
"inserted_at": "2026-06-03T10:00:00Z"
}
}
/api/shorts/:slug/text/generate
1 AI assist
Générer un titre, un sous-titre et des lignes de légende modifiables à partir du brouillon de short enregistré (remplit aussi brand_name / closing_claim s’ils sont vides — uniquement par extraction et reformulation, jamais de nouvelles allégations). Rien n’est prélevé sur le solde et aucun rendu ne démarre. En général, c’est inutile avant la génération : generate rédige gratuitement le texte manquant.
curl -X POST https://hubfluencer.com/api/shorts/amber-candle-9x2/text/generate \ -H "authorization: Bearer $HF"
/api/shorts/:slug/generate
1.60
video:generate
Lancer le rendu du short. Idempotent par slug. Pollez ensuite GET /api/shorts/:slug. Texte automatique : quand headline, subheadline ET text_beats sont tous vides, le serveur rédige d’abord le texte d’incrustation à la volée (gratuitement — cela ne touche jamais le quota quotidien d’assists IA) et l’enregistre AVANT de facturer, si bien que les relances ne le régénèrent jamais ; passez {"skip_auto_text": true} dans le corps pour un clip volontairement sans texte. Si le générateur de texte est indisponible → 503 copy_generation_unavailable, rien n’est facturé — il suffit de relancer. Description automatique : une image produit associée sans description est décrite à la volée au moment de generate (gratuitement aussi — l’endpoint autonome product-description/generate reste décompté) afin que les segments IA portent l’ancrage produit ; si la description échoue, la génération continue simplement sans. Le prompt (texte rédigé automatiquement compris) est contrôlé au regard de la politique de contenu AVANT toute facturation : un prompt interdit → 422 prompt_rejected (code CONTENT_COMPLIANCE, avec une catégorie) ; contrôle indisponible → 503 compliance_unavailable (fermeture par défaut) — aucun des deux n’est facturé. Un solde insuffisant → 402 balance_insufficient avec required + available (objets monétaires).
curl -X POST https://hubfluencer.com/api/shorts/amber-candle-9x2/generate \ -H "authorization: Bearer $HF" \ -H "idempotency-key: gen-short:amber-candle-9x2"
/api/shorts/:slug/rerender
free
video:generate
Le nouveau rendu est GRATUIT, pour toujours — vous ne payez jamais que les nouvelles images IA. Recompose l’état ACTUEL de l’incrustation et du style du short sur les segments déjà générés (épinglés) + la musique : modifiez headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, les couleurs, short_font_family, short_text_position, short_text_animation, accent_color, closing_claim, brand_name, poster (+ poster_includes_lockup), l’incrustation du logo (image via /logo/confirm, position, treatment) ou end_card via PATCH, puis appelez cet endpoint (sans corps). Renvoie le même payload Short que generate — suivez generation_summary sur GET /api/shorts/:slug. Composition inchangée → le rendu existant est renvoyé tel quel (idempotent). La piste musicale épinglée est réutilisée même quand une modification de la carte de fin a changé la durée — une piste plus courte enchaîne en fondu sur une continuation répétée et s’estompe à la fin de l’export au lieu de faire échouer le rendu. Les champs qui touchent les images ou la musique (prompt, image produit, creative_format, visual_language, theme, music_vibe, music_instruments, language) nécessitent plutôt le generate payant ; en modifier un renvoie 422 short_paid_regeneration_required. Un nouveau rendu gratuit en ÉCHEC ne casse jamais le short livré : le résumé canonique signale la tentative échouée tandis que l’étape de projection produit reste video_ready, latest_render se rabat sur le dernier rendu terminé, et last_free_rerender_failed: true est défini (latest_render_free signale toute ligne de nouveau rendu gratuit la plus récente). Relisez le résumé et n’invoquez qu’une action proposée. 409 short_generation_in_progress tant qu’un travail est actif ; 422 short_not_ready tant que le short n’a pas ses 2 segments terminés, sa musique terminée et un rendu terminé. Jamais de 402.
# edit any free field first (free), then re-render — free, no body
curl -X PATCH https://hubfluencer.com/api/shorts/amber-candle-9x2 \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-d '{"cta_text": "Order today"}'
curl -X POST https://hubfluencer.com/api/shorts/amber-candle-9x2/rerender \
-H "authorization: Bearer $HF" \
-H "idempotency-key: rerender-short:amber-candle-9x2:v2"
/api/shorts/:slug
video:read
Lisez le generation_summary obligatoire. Ne pollez que tant que timing.poll_after_ms est un entier positif, en respectant exactement cette cadence du serveur ; null signifie qu’il faut arrêter. status détermine l’état terminal, blockers/progress/failure/cost proviennent du résumé, et seul available_actions peut activer la relance, l’annulation ou le téléchargement. failure n’est non nul qu’en cas de véritable échec ou d’annulation — un run en attente sur needs_input n’est pas un échec : failure reste à null et l’attente est décrite par blockers plus l’action d’input proposée. download_result n’est proposé que pour un artefact de livraison prêt et vérifié. generation_summary.run_id est la barrière de route explicite renvoyée à /cancel. Les champs de projection produit comme stage, les statuts enfants, segments_completed / segments_total / active_segment_positions, latest_render et last_free_rerender_failed ne sont que des données d’affichage ; ils ne prévalent jamais sur le résumé.
200 OK — poll only while generation_summary.timing.poll_after_ms is positive
{
"data": {
"slug": "amber-candle-9x2",
"generation_summary": {
"run_id": "5d379ddd-6687-41cb-a1be-d2684875335f",
"workflow": "short.generation", "workflow_version": 1, "revision": 14,
"status": "completed", "settling": false, "phase": "completed", "label": "Video ready",
"progress": {"completed": 4, "total": 4, "percent": 100}, "current_step": null,
"blockers": [],
"available_actions": [{"type":"download_result","label":"Download video","destructive":false,"step_key":null}],
"failure": null,
"cost": {"quoted":{"amount_minor":160,"currency":"EUR"},"charged":{"amount_minor":160,"currency":"EUR"},"refunded":{"amount_minor":0,"currency":"EUR"},"net":{"amount_minor":160,"currency":"EUR"},"max":{"amount_minor":160,"currency":"EUR"}},
"timing": {"started_at":"2026-06-03T10:00:00Z","updated_at":"2026-06-03T10:03:00Z","deadline_at":null,"poll_after_ms":null}
},
"stage": "video_ready",
"failed_stage": null,
"failure_code": null,
"failure_details": null,
"failure_source": null,
"failure_attempt": null,
"segments_completed": 2,
"segments_total": 2,
"active_segment_positions": [],
"error_message": null,
"latest_render": {
"status": "completed",
"video_url": "https://…/amber-candle-9x2.mp4?X-Amz-Expires=86400"
}
}
}
/api/shorts/:slug/cost
video:read
Vérification préalable du coût — renvoie total et available (objets monétaires) avant toute dépense.
/api/shorts/:slug
free
video:generate
Modifier un brouillon avant le rendu : n’importe quel champ créatif de POST /api/shorts (product_prompt, headline, subheadline, creative_format, visual_language, music_vibe, les éléments de conversion optionnels, …). Mise à jour partielle — seuls les champs envoyés changent. Pour RETIRER un élément optionnel, envoyez-le vide : offer_text/cta_text/badge_text "", star_rating 0, text_beats []. Accepte aussi les réglages de transparence IA ai_disclosure_mode / ai_disclosure_position / ai_disclosure_ack (le mode "none" exige l’ack dans la même requête → sinon 422 ai_disclosure_ack_required ; voir Transparence IA & conformité). Gratuit, sans rendu. Pendant la génération du short (processing), les modifications sont verrouillées → 409 short_generation_in_progress ; attendez qu’elle se termine ou échoue.
Images de marque — produit, logo & affiche de carte de fin
Tous sont gratuits et suivent le flux presign → PUT des octets bruts → confirm. Sur le PUT unique, envoyez un Content-Type correspondant au mime présigné. Les images produit acceptent le jpeg/png jusqu’à 20 Mo et sont intégrées aux séquences générées. Les logos acceptent le jpeg/png jusqu’à 5 Mio et sont composités de façon déterministe par Forge, jamais envoyés au modèle vidéo. Les affiches de carte de fin acceptent aussi le webp et sont limitées à 20 Mo. Une image jamais importée, trop volumineuse ou corrompue est refusée au confirm (422).
Incrustation de tracking
Importez votre propre vidéo et récupérez-la avec une surcouche de tracking par vision par ordinateur incrustée par-dessus (polygones d’enveloppe convexe épousant chaque sujet en mouvement, crochets HUD, un réticule et une télémétrie par sujet). Le clip source doit durer 60 s au plus et ne pas dépasser le 1080p. Créez un brouillon (gratuit), définissez le clip source — importez-le via les routes d’import editor avec le slug de tracking, ou réutilisez une vidéo de la bibliothèque via /source/from-asset — puis lancez le rendu (0,20). Le clip source est supprimé une fois le rendu terminé.
/api/tracking
free
Créer un brouillon de tracking. Corps optionnel : language. Renvoie le slug ; importez le clip source via POST /api/editor/:slug/uploads/* (presign ou multipart, puis confirm) avec le slug renvoyé.
/api/tracking
video:read
Lister vos projets de tracking en cours (la liste des travaux en cours de Studio) — les projets terminés n’y figurent pas. Chaque entrée a la même forme que GET /api/tracking/:slug.
/api/tracking/:slug
video:read
Poller le projet. La réponse contient source (le dernier import source) et result. source.status passe par pending → processing → ready (ou failed) ; attendez "ready" avant de lancer le rendu, et considérez une source ni ready ni failed comme encore en cours d’import ou de traitement. Une fois le rendu effectué, lisez result.status ("completed") + result.video_url ; en cas d’échec du rendu, lisez result.error_message.
/api/tracking/:slug/source/from-asset
free
video:generate
Réutiliser comme clip source une vidéo déjà présente dans votre bibliothèque d’assets au lieu d’en importer une nouvelle. Corps : asset_id (un asset vidéo du catalogue, prêt, qui vous appartient). L’objet de l’asset est copié dans le projet et normalisé (l’asset de votre bibliothèque reste intact) : pollez donc GET /api/tracking/:slug jusqu’à ce que source.status vaille "ready", puis lancez le rendu. 422 asset_not_video / asset_not_ready si l’asset n’est pas une vidéo prête ; 409 tracking_render_in_progress tant qu’un rendu est en cours.
/api/tracking/:slug/render
0.20
video:generate
Incruster la surcouche. Idempotent par slug (envoyez une Idempotency-Key). Corps optionnel : classes (ids COCO-80 à suivre, p. ex. [0]=person, [16]=dog ; omettez-le pour tout suivre) et color (surcouche [r,g,b,a]). Un nouveau rendu identique renvoie gratuitement le résultat existant. Exécution unique : un rendu lancé alors qu’un autre est encore en cours renvoie 409 tracking_render_in_progress (sans frais supplémentaires). Un solde insuffisant renvoie 402 balance_insufficient avec required + available. Pollez ensuite GET /api/tracking/:slug. Le clip source est supprimé une fois le rendu terminé (vous gardez le résultat) ; définissez une nouvelle source pour relancer un rendu.
/api/tracking/:slug
Supprimer un projet de tracking avec son clip importé + son résultat. Le supprimer en cours de rendu rembourse les 0,20. Nécessite le scope video:generate (comme toutes les méthodes d’écriture).
Sliders (carrousels d’images)
Un slider est un carrousel pour les réseaux sociaux : un prompt produit N diapositives fixes (un fond IA + un titre/texte composité + un logo optionnel), plus une légende et des hashtags prêts à publier. Pas de vidéo — enregistrez les images et copiez le texte. Créez un brouillon (gratuit), puis lancez la génération (0,15 par diapositive ⇒ 0,45–1,50 ; 5 diapositives par défaut = 0,75). La génération est asynchrone : pollez GET /api/sliders/:slug jusqu’à ce que status vaille completed ou failed. Modifier le texte d’une diapositive, ou le template, l’accent ou le logo, relance un rendu gratuit.
/api/sliders
free
Créer un brouillon de carrousel.
| Champ du corps | Type | Remarques |
|---|---|---|
prompt optionnel |
string | Le sujet du carrousel. ≤2000 caractères. Optionnel au stade du brouillon, requis (≥10 caractères non vides) avant generate. |
mode optionnel |
string | creative (storytelling) ou ad_driven (faits produit). creative par défaut. |
template optionnel |
string | boldStatement / editorialStory / scrapbook (creative) ; featureGrid / offerCard / comparison (ad-driven). Par défaut, celui du mode. |
language optionnel |
string | Langue dans laquelle sont rédigés le texte des diapositives et la légende générés : en (par défaut) / fr / es / de / it / pt / nl / pl. Alphabet latin uniquement. |
slide_count, aspect_ratio, accent_color
optionnel
|
integer / string | slide_count entier de 3 à 10 (5 par défaut) ; aspect 4:5 (par défaut) / 1:1 / 9:16 ; accent, une couleur hexadécimale correspondant à ^#[0-9a-fA-F]{6}$, comme #09EFBE. |
text_position optionnel
|
string | top / middle / bottom — position verticale du texte incrusté sur toutes les diapositives. Omettez-le pour utiliser la position naturelle du template. |
caption, hashtags
optionnel
|
string / array<string> | Texte de publication : caption ≤3000 caractères ; hashtags est un tableau de chaînes. Généralement rédigé par generate, mais modifiable via PATCH. |
/api/sliders/:slug/generate
0.15 / slide (3–10)
video:generate
Lancer le rendu du carrousel. Coûte 0,15 par diapositive ⇒ 0,45–1,50 (5 diapositives par défaut = 0,75). Envoyez une nouvelle Idempotency-Key à chaque tentative (une clé fixe par slug rejoue la première réponse pendant 24 h et bloque une régénération volontaire). Le prompt est d’abord contrôlé au regard de la politique de contenu — un prompt non conforme renvoie 422 prompt_rejected (code CONTENT_COMPLIANCE, avec une catégorie) et n’est pas facturé. Limité à 10/min. Pollez ensuite GET /api/sliders/:slug jusqu’à ce que status vaille completed ou failed.
/api/sliders/:slug
video:read
Pollez le statut jusqu’à un état terminal : draft → processing → completed (chaque slides[].image_url est une image fixe téléchargeable ; lisez caption + hashtags) | failed (lisez error_message ; le montant est remboursé). Chaque entrée de slides[] est { position, headline, body, kicker, status, image_url, background_url } ; le statut par diapositive est pending → processing → completed | failed (une diapositive pas encore rendue est pending, pas draft). image_url est la diapositive finale composée (présignée, TTL de 24 h) ; background_url est l’aperçu brut du fond IA (présigné, TTL de 1 h).
/api/sliders/:slug/slides/:position
free
video:generate
Modifier le texte incrusté d’une diapositive : headline (≤120), body (≤600), kicker (≤40). Recompose gratuitement cette seule diapositive (le fond IA est réutilisé — aucun coût d’image supplémentaire). Le carrousel doit déjà être terminé (sinon 409 conflict) ; l’opération est asynchrone et repasse la diapositive en processing — pollez GET /api/sliders/:slug jusqu’à ce qu’elle repasse en completed. Limité à 20/min.
/api/sliders/:slug/restyle
free
video:generate
Changer le style d’un carrousel terminé : le corps accepte template, accent_color (^#[0-9a-fA-F]{6}$), text_position (top/middle/bottom) et logo_s3_key — et recompose gratuitement CHAQUE diapositive (les fonds IA sont réutilisés). Le carrousel doit déjà être terminé (sinon 409). Asynchrone (chaque diapositive → processing) ; pollez GET /api/sliders/:slug jusqu’à completed. Limité à 20/min. (Confirmer un logo via /logo/confirm sur un carrousel déjà terminé passe par restyle : cela déclenche donc aussi une recomposition complète de chaque diapositive, gratuite.)
/api/sliders
Lister vos carrousels (du plus récent au plus ancien). GET /api/sliders/:slug/cost renvoie total + available (objets monétaires). PATCH /api/sliders/:slug modifie toujours les champs de texte (caption, hashtags) ; le prompt et les champs qui déterminent le rendu (language, mode, template, slide_count, aspect_ratio, accent_color, text_position, logo_s3_key) ne sont modifiables ici que tant que le slider est draft ou failed (un rendu échoué les rouvre) — une fois processing/completed, ils sont ignorés silencieusement (le prompt est verrouillé après la génération ; utilisez plutôt restyle / le PATCH de diapositive). DELETE /api/sliders/:slug supprime un carrousel et ses images ; il renvoie 409 slider_generation_in_progress tant qu’un rendu est en cours (un rendu échoué rembourse lui-même son coût, attendez donc que le rendu se termine). POST /api/sliders/:slug/logo/presign puis /logo/confirm associent un logo de marque optionnel, ou POST /api/sliders/:slug/logo/from-asset {asset_id} réutilise une image du catalogue prête qui vous appartient (gratuit).
Pubs editor — autopilot
Une pub editor est une vidéo multi-scènes portée par une histoire. Autopilot exécute tout le pipeline côté serveur (scénario → scènes → narration → voix → musique → rendu). Créez le projet avec une cible exacte de 3 à 10 scènes, lancez autopilot, puis suivez generation_summary ; ne téléchargez que lorsque ses available_actions proposent download_result. Un run est facturé par scène (sans remise sur les lots), plus la voix (0,15 par tranche entamée de 1 000 caractères de script) et la musique (0,25 par minute, facturée à la seconde), toutes deux dimensionnées sur la durée de la timeline ; vérifiez le montant exact au préalable avec GET /api/editor/:slug/autopilot/cost.
POST /api/editor/:slug/autopilot sélectionne de façon transparente la topologie inversée editor.autopilot.voicefirst@1 quand le projet est en rythme voice_first (voir Ossature vocale — modes de rythme). Son ordre est scenario.compile → scenario.review → scenario.apply → narration → voice → pacing.derive → previews → scenes (générées aux durées dérivées) → scene reviews → music → render → delivery. Le contrat de requête et de réponse est inchangé : l’appelant ne choisit jamais de workflow, il règle seulement le mode de rythme.
Chaque lecture produit Editor et Short inclut un generation_summary non nul. Traitez-le comme la seule autorité sur le cycle de vie, même quand stage, autopilot_status, batch_generation_status, les statuts enfants ou latest_render divergent. Ne pollez que tant que generation_summary.timing.poll_after_ms est un entier positif, exactement à cette cadence du serveur ; null signifie qu’il faut arrêter de poller. Lisez l’état terminal dans generation_summary.status, n’invoquez que generation_summary.available_actions et ne téléchargez que lorsque download_result est proposé pour un artefact de livraison vérifié. Les champs de projection produit ne sont que des métadonnées d’affichage ou des barrières de route explicites ; n’en déduisez jamais la disponibilité du polling, de la relance, de l’annulation ou du téléchargement.
/api/editor
free
Créer un projet editor. Les comptes dont le solde est à zéro peuvent détenir jusqu’à 100 projets editor — un 101e renvoie 402 editor_factory_limit_reached ; supprimez-en un ou ajoutez des fonds. Les comptes approvisionnés n’ont pas de plafond. Envoyez un en-tête Idempotency-Key pour qu’une relance ne crée pas un second brouillon.
| Champ du corps | Type | Remarques |
|---|---|---|
language requis |
string | Code pris en charge : en, fr, es, de, it, nl, pl, pt, ru, zh, ja, ko ou ar. Pilote TOUTE la vidéo : texte du scénario, script de narration, voix de narration et sous-titres. |
product_prompt |
string | Vide OU 10–5000 caractères. Autopilot a besoin d’un brief exploitable : un brief de scénario structuré, product_prompt ≥10 caractères, OU product_subject ≥3 caractères. |
product_subject optionnel
|
string | ≤ 300 caractères. Le produit concret ou le sujet principal dont la vidéo doit parler (p. ex. « vin rouge de Bordeaux, bouteille vert foncé à étiquette dorée ») — distinct du brief libre product_prompt. Lorsqu’il est défini, le scénario et la narration IA s’y ancrent strictement et ne peuvent pas inventer un produit sans rapport (l’échec le plus courant des vidéos publicitaires). Recommandé pour project_intent social_ad. Omettez-le ou laissez-le vide pour laisser l’IA libre. |
segments_count optionnel
|
integer | 3–10. Cible structurelle exacte pour le scénario, le devis et le lancement d’Autopilot. Ne crée pas de lignes fictives dans la timeline. |
export_aspect_ratio
optionnel
|
enum | 9:16 (par défaut), 16:9 ou 1:1. |
creative_format, visual_language
optionnel
|
enum | Contrôles créatifs (mêmes listes de valeurs que Shorts) : arc narratif + rendu visuel. Omettez-les pour Auto. |
theme, voice_id, voice_model,
project_intent
optionnel
|
string | Visual theme / genre overlay (when visual_language is set it drives the look; "none" = no imposed style), narration voice, narration model ("standard" or "expressive": Eleven v3, performs tags such as [excited] written in the narration; same price), social_ad | creative_story. |
Requête (création, puis lancement d’autopilot)
curl -X POST https://hubfluencer.com/api/editor \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-d '{"language":"en","product_prompt":"a cinematic ad for my candle brand","segments_count":3,"export_aspect_ratio":"9:16"}'
# then start autopilot:
ATTEMPT_ID="$(uuidgen)"
curl -X POST https://hubfluencer.com/api/editor/$SLUG/autopilot \
-H "authorization: Bearer $HF" -H "idempotency-key: autopilot:$SLUG:$ATTEMPT_ID"
/api/editor/:slug/autopilot
devis plafonné
video:generate
Exécuter tout le pipeline. Envoyez une Idempotency-Key : ne la réutilisez que pour les relances réseau de ce même lancement logique, et créez-en une nouvelle pour une reprise, un redémarrage, un changement de surcharge ou un changement de plafond délibérés (les réponses réussies sont rejouées pendant 24 h sous l’ancienne clé). Vérifiez le coût au préalable avec GET /api/editor/:slug/autopilot/cost en utilisant exactement les mêmes surcharges de lancement. Accepte un corps JSON OPTIONNEL : language (l’un des codes de projet pris en charge), product_subject (3–300 caractères s’il n’est pas vide), product_prompt (10–5000 caractères s’il n’est pas vide), segments_count (cible structurelle exacte de 3 à 10), max_cost (entier en unités mineures de la devise du compte, 0..1000000 ; une estimation supérieure est refusée avec 422 max_cost_exceeded avant tout démarrage) et restart (booléen). Un brief de scénario structuré, product_prompt ≥10 ou product_subject ≥3 satisfont l’exigence de brief. Pour un projet terminé, envoyez restart=true à la fois dans la requête de coût et dans le corps de lancement pour retirer la timeline IA actuelle et construire un nouveau run créatif ; omettez-le pour ne reprendre que les modifications en attente. Le plafond est enregistré pour ce run et de nouveau appliqué de façon atomique à chaque prélèvement de scène, de voix et de musique ; s’il devait être dépassé, aucun autre prélèvement ne démarre. Omettez product_subject/product_prompt pour les laisser inchangés ; envoyez null ou une valeur vide pour effacer l’un ou l’autre de façon atomique au lancement. Des valeurs de requête vides chiffrent ces effacements sans les enregistrer.
Requête (corps optionnel — surcharges avant lancement)
ATTEMPT_ID="$(uuidgen)"
curl -X POST https://hubfluencer.com/api/editor/$SLUG/autopilot \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-H "idempotency-key: autopilot:$SLUG:$ATTEMPT_ID" \
-d '{"language":"fr","product_subject":"Bordeaux red wine, dark green bottle with a gold label","product_prompt":"a concise cinematic social ad for my wine","segments_count":3,"max_cost":370}'
Réponse (pollez GET /api/editor/:slug)
200 OK — generation_summary owns lifecycle, cadence, and action availability
{
"data": {
"slug": "candle-story-7k1",
"generation_summary": {
"run_id": "60d15a8b-b987-4412-838e-3441b89eeb6d",
"workflow": "editor.autopilot", "workflow_version": 1, "revision": 42,
"status": "completed", "settling": false, "phase": "completed", "label": "Video ready",
"progress": {"completed": 8, "total": 8, "percent": 100}, "current_step": null,
"blockers": [],
"available_actions": [{"type":"download_result","label":"Download video","destructive":false,"step_key":null}],
"failure": null,
"cost": {"quoted":{"amount_minor":370,"currency":"EUR"},"charged":{"amount_minor":370,"currency":"EUR"},"refunded":{"amount_minor":0,"currency":"EUR"},"net":{"amount_minor":370,"currency":"EUR"},"max":{"amount_minor":370,"currency":"EUR"}},
"timing": {"started_at":"2026-06-03T10:00:00Z","updated_at":"2026-06-03T10:08:00Z","deadline_at":null,"poll_after_ms":null}
},
"autopilot_status": "completed",
"autopilot_contract_satisfied": true,
"autopilot_error_message": null,
"scenario_prompt": "Warm, handcrafted… ",
"segments_count": 5,
"segments": [
{ "id": 81, "position": 1, "prompt": "Macro of wax pouring…", "status": "completed" },
{ "id": 82, "position": 2, "prompt": "Hands trimming the wick…", "status": "completed" }
],
"narration_script": "Made by hand, poured in small batches…",
"narration_script_source": "manual",
"narration_status": "ready",
"current_audio": { "status": "completed" },
"current_music": { "status": "completed" },
"latest_render": {
"status": "completed",
"video_url": "https://…/candle-story-7k1.mp4?X-Amz-Expires=86400",
"is_stale": false
}
}
}
La narration utilise narration_status (none → generating → ready | failed). Les prompts de production sont des directions visuelles ; un narration_script fourni manuellement est lu mot pour mot et ne doit contenir que les mots exacts prononcés. Les médias générés sélectionnés sont current_audio et current_music — pas audio/music — et chaque version sélectionnée passe par pending → processing → completed | failed. Un latest_render terminé peut être historique après des modifications ; ne le considérez comme actuel que si is_stale vaut false.
/api/editor/:slug/autopilot/cancel
free
video:generate
À invoquer uniquement quand generation_summary.available_actions propose cancel_generation. Idempotency-Key est obligatoire ; ne la réutilisez que pour les relances réseau de ce run précis observé. Envoyez {generation_run_id: generation_summary.run_id}. La barrière empêche une requête antérieure retardée d’annuler un run successeur. N’arrête que cette chaîne observée, en conservant le travail déjà facturé et terminé ; relisez generation_summary après un conflit dû à une barrière périmée.
Pipeline de l’éditeur granulaire
Le même projet editor, piloté étape par étape au lieu d’autopilot — rédigez le scénario, fixez le nombre de scènes, écrivez à la main le prompt de chaque scène, puis générez. Les lectures nécessitent video:read ; tout ce qui génère nécessite video:generate. Chaque ligne est free (sans frais, sans assist), assist (1 assist IA) ou paid (prélevé sur le solde).
* render facture automatiquement chaque scène encore manquante au prix de génération de son modèle sélectionné (il n’y a pas de remise sur les lots) — y compris les scènes IA précédemment en ÉCHEC, qui sont relancées et facturées comme travail manquant (les imports en échec bloquent toujours le rendu) — si bien qu’un projet entièrement généré est rendu gratuitement. Les scènes historiques seedance-2.0 sont de nouveau sélectionnables ; elles sont chiffrées et facturées automatiquement au prix actuel du catalogue pour ce modèle, alors qu’elles échouaient auparavant comme modèle indisponible. Avant de dépenser, lisez les video_models Editor en vigueur et les scènes actuelles, établissez le devis de la cohorte exacte et ordonnée segment/modèle/version de tarif, obtenez l’accord, puis envoyez max_cost et generation_quote_fingerprint ; un devis périmé renvoie 409 generation_quote_changed sans rien lancer. S’il ne manque rien, envoyez seulement l’empreinte vide et aucun max_cost : une scène ajoutée entre-temps fait alors échouer la barrière au lieu d’être facturée. voice_id correspond à ^[A-Za-z0-9_-]+$ (≤64). Le rendu attend la fin des générations de narration, de voix ou de musique en cours (409 narration_generation_in_progress / voice_generation_busy / music_generation_busy). Une timeline qui dépasse le plafond de 600 s renvoie 422 editor_total_duration_exceeded ; quand le remboursement d’une scène qui vient d’échouer est encore en cours de règlement, render renvoie un 409 editor_render_settlement_pending qui peut être relancé (réessayez sous peu). Modifier le scénario ou la narration après avoir généré la voix ou la musique les rend obsolètes — render renvoie 422 editor_voice_stale / editor_music_stale / editor_narration_stale jusqu’à ce que vous les régénériez.
generate-scenario rédige le scénario de façon asynchrone : son 202 indique déjà scenario_status "generating" — y compris lors d’une régénération, pour qu’un poll ne puisse jamais confondre le précédent scénario "ready" avec le nouveau brouillon — et GET /api/editor/:slug reste sur "generating" jusqu’à l’arrivée du nouveau brouillon ("ready" = relisez-le, "failed" = relancez). apply-scenario matérialise le scénario enregistré dans la timeline : les scènes IA existantes sont remplacées par des prompts de scène générés à partir du scénario, les imports sont conservés, et segments_count 0 supprime les scènes IA. C’est gratuit — ni prélèvement ni assist IA (generate-scenario a déjà consommé l’assist) — et asynchrone : le 202 indique déjà scenario_apply_status "pending", pollez donc GET /api/editor/:slug jusqu’à ce qu’il quitte "pending"/"applying" ("idle" = terminé, "failed" = relancez). Si segments_count est omis, apply utilise le nombre de scènes du brief de scénario enregistré, puis 5 par défaut. Lisez video_models dans la réponse Editor avant de dépenser : c’est toute la flotte sélectionnable, chaque entrée portant tier, lifecycle, capabilities (dont durations_seconds et native_audio), les implications de continuité, les avis et ses prix. generate/regenerate acceptent un id de modèle optionnel et un objet generation_options optionnel portant duration_seconds (l’une des capabilities.durations_seconds de ce modèle ; omettez-le pour la valeur par défaut du modèle, 8 s) et native_audio. price.generation/regeneration/batch_discounted sont des objets monétaires à cette durée par défaut et au réglage audio par défaut du modèle (les trois sont égaux : une régénération coûte autant qu’une génération et il n’y a pas de remise sur les lots) ; toute autre combinaison est une ligne de price_table {duration_seconds, native_audio, generation, regeneration, batch_discounted}. Lisez la ligne à laquelle la tentative s’exécutera ; ne calculez jamais un prix côté client. Une durée non prise en charge renvoie 422 unsupported_generation_duration et tout autre nom d’option renvoie 422 invalid_generation_options — aucun des deux ne crée de run ni ne facture. Les scènes générées AVEC des images de référence sont en plus soumises à capabilities.reference_durations_seconds — [8] sur la famille veo3.1 (veo3, veo3-fast, veo3-lite), où la route reference-to-video du fournisseur refuse définitivement toute autre durée ; une scène conditionnée par des références dont la durée effective sort de cette grille donne 422 editor_reference_duration_unsupported, dont les details nomment la grille (required) et la durée refusée (supplied), et elle est refusée avant qu’un run ou une facturation n’existe. SCÈNES DE DIALOGUE : native_audio true conserve le son propre du modèle pour cette scène et enregistre use_original_audio sur le segment (PATCH /api/editor/:slug/segments/:id accepte directement le même indicateur). C’est conditionné par capabilities.native_audio.mode du modèle : optional accepte les deux valeurs (false par défaut), always n’accepte que true (sinon 422 native_audio_required — wan-2.7, happy-horse-1.1 et minimax-h3 renvoient du son quoi que vous demandiez et sont tarifés en conséquence), et unavailable n’accepte que false (422 native_audio_not_available). Une scène de dialogue est tarifée selon la ligne avec audio de la price_table du modèle, qui sur Veo 3.1 Standard vaut presque le double du prix muet : une scène veo3 de 8 s coûte 2,55 en muet et 4,95 avec audio. Quand l’audio natif se résout à TRUE, l’empreinte du devis gagne un marqueur audio littéral avant la version de tarif — six parties, segment_id:model:<operation>:duration_seconds:audio:pricing_version, par exemple 7:veo3:generation:8:audio:2026-09-24-cost-v1 à 4,95. Le marqueur suit la valeur RÉSOLUE, et non la présence de l’option dans votre requête : un modèle en mode always comme une scène déjà passée à use_original_audio: true exigent tous deux la forme marquée, même sans generation_options dans le corps ; la forme muette en cinq parties est alors refusée avec 409 generation_quote_changed, car elle encode un prix qui ne vous a jamais été présenté. Les variantes se protègent de la même façon (segment_id:model:variants:count:duration_seconds:audio:pricing_version) ; l’empreinte de cohorte des lots est inchangée et ignore l’audio, même si un lot tarifie toujours chaque scène selon son propre indicateur enregistré. Une scène de dialogue ne parle jamais : elle est exclue de la narration, la piste de voix pose un silence numérique en dessous, le rendu coupe entièrement la narration sur toute sa durée, et en rythme voice_first c’est un intervalle de narration fixe dont l’ossature ne modifie jamais la durée (basculer l’indicateur active pacing_stale). Chaque segment porte aussi un audio_role dérivé, calculé et jamais envoyé : dialogue dès que la scène joue son PROPRE audio (use_original_audio true — la même réponse pour une prise IA qui garde le son du modèle et pour un import qui garde l’audio de son enregistrement ; distinguez-les avec source_type), narrated quand une ligne de voix off tombe dessus (son propre narration_text avec narration_script_source "segments", et toute scène sans dialogue avec "manual", où l’unique script du projet est réparti sur l’ensemble des scènes parlées au moment de la voix), et ambient sinon. Protégez un devis approuvé pour une scène avec max_cost et generation_quote_fingerprint sous la forme segment_id:model:<operation>:duration_seconds:pricing_version, où operation vaut generation ou regeneration, duration_seconds est toujours présent — la durée à laquelle la tentative s’exécute : un generation_options.duration_seconds explicite, sinon la durée définie de la scène, sinon la valeur par défaut du modèle, 8 s — et pricing_version est la version actuelle de l’entrée que vous venez de lire, par exemple 7:veo3:generation:8:2026-09-24-cost-v1. La forme en quatre parties sans durée segment_id:model:<operation>:pricing_version est refusée avec 409 generation_quote_changed, même à la durée par défaut du modèle. Les versions de tarif antérieures (2026-07-31, 2026-08-09-unified-v1, 2026-09-23-money-v1) ne sont jamais acceptées. Un modèle, une durée ou une version de tarif périmés renvoient 409 generation_quote_changed (required, max_cost, current_pricing_version) avant le lancement. batch-generate génère chaque scène à la MÊME durée que si elle était générée individuellement : la valeur par défaut du modèle en scene_first, et l’authored_duration_seconds de la scène en voice_first — et c’est désormais aussi à cette durée qu’il est tarifé, si bien qu’un lot et les générations unitaires équivalentes chiffrent la même durée et le même prix par scène (il n’y a pas de tarif lot). Un generation_options.duration_seconds explicite reste accepté uniquement sur generate/regenerate d’une seule scène. L’Autopilot voice-first fige le plafond de durée sélectionnable comme dépense maximale approuvée, puis résout le prélèvement exact de chaque scène à partir de sa table immuable de prix par durée après pacing.derive ; l’Autopilot scene-first reste tarifé à la durée par défaut du modèle. regenerate crée une NOUVELLE version d’une scène ; passez prompt et model pour définir la nouvelle version, ou omettez l’un ou l’autre pour réutiliser la sélection actuelle. Une scène terminée ne peut pas être relancée via generate (422 editor_segment_completed) — regenerate est la voie vers une nouvelle prise. En rythme voice_first, tout cela reste valable, avec un ajout : quand une scène a un authored_duration_seconds, CETTE durée définie est la durée tarifée. C’est vers elle que se résolvent la table de prix, le generation_quote_fingerprint et max_cost quand la requête ne porte pas de generation_options : construisez donc l’empreinte v2 en cinq parties segment_id:model:<operation>:duration_seconds:pricing_version à partir de l’authored_duration_seconds servi (une scène rythmée à 6 s a une empreinte à 6, pas à la valeur par défaut du modèle de 8 s). Un generation_options.duration_seconds explicite l’emporte toujours, mais il doit être ÉGAL à la durée définie, sinon l’appel est refusé avec 422 editor_pacing_duration_mismatch (avec les valeurs définie et demandée dans details), afin que ce que l’ossature a défini et ce qui est réellement généré ne puissent jamais diverger en silence. Un projet dans lequel CHAQUE scène garde son propre audio n’a plus rien à dire : narration, generate-voice, voice-plan et autopilot le refusent tous d’emblée avec 422 editor_no_speaking_segments, avant qu’aucune scène ne soit facturée. Et comme l’audio d’une prise est fixé à sa génération, un PATCH de use_original_audio sur une scène IA qui a déjà une prise renvoie 409 editor_segment_audio_locked — régénérez avec generation_options.native_audio pour le changer. EMPLACEMENTS DE RÉFÉRENCE (identité) : les références fixent l’identité, l’enchaînement par dernière image fixe la continuité, et une génération avec références envoie les deux — d’abord les images d’identité de la scène, puis l’image de transition visible de la scène précédente, ajoutée en dernier — dans la limite du budget d’emplacements du modèle ; remplir tous les emplacements supprime l’image de continuité et définit continuity_limited: true. Chaque segment porte reference_s3_keys (clés de stockage ordonnées) et reference_urls (les mêmes emplacements présignés pour l’affichage, ALIGNÉS PAR POSITION : reference_urls[i] montre reference_s3_keys[i], et un emplacement impossible à signer contient null au lieu d’être supprimé — renvoyez les clés, jamais les URL). Un ensemble non vide REMPLACE la photo produit du projet pour cette scène au lieu de s’y ajouter, car les emplacements sont rares et l’empilement ferait sortir l’image de continuité de la requête. Le nombre accepté par un modèle est capabilities.reference_images_max dans son entrée video_models — un entier à côté du booléen existant capabilities.reference_images, qui vaut exactement reference_images_max > 0 : veo3 et veo3-fast en acceptent 3, seedance-2.0 et seedance-2.0-fast en acceptent 9, et tous les autres modèles 0. Le dépasser donne 422 editor_reference_images_exceed_model_max, dont les details nomment le modèle et son max_images exact, et c’est refusé à l’admission de la génération comme au moment de l’association. Les références ne changent jamais le prix et n’entrent jamais dans l’empreinte du devis : la route de référence appartient à la même famille d’endpoints fal que les autres routes du modèle et est tarifée exactement comme elles (l’avis reference_route_same_price l’indique sur les modèles où elle vient d’être branchée). Les DURÉES sont la seule asymétrie : la route de référence n’accepte que capabilities.reference_durations_seconds — [8] sur veo3 et veo3-fast, où l’avis reference_duration_locked le signale ; égal à durations_seconds partout ailleurs — donc une scène qui envoie des références d’identité (ses propres reference_s3_keys, ou la photo produit du projet sur un modèle compatible avec les références) doit être une scène de 8 secondes sur ces modèles. Toute autre durée donne 422 editor_reference_duration_unsupported avant toute facturation, avec details.required et details.supplied. 4 s et 6 s restent entièrement disponibles pour les scènes sans références, et en rythme voice_first l’ossature dérive automatiquement les scènes conditionnées par des références sur la grille de référence — une ossature rythmée AVANT l’association des références est refusée avec le même 422 jusqu’à ce que le prochain plan de voix la rythme de nouveau. Associez des images à une scène sans prise via references/presign puis references/confirm (image/jpeg, image/png ou image/webp, 8 Mo au plus), ou via references/from-asset pour une image du catalogue ; retirez-les avec DELETE .../references/:index. Ces quatre routes sont réservées aux scènes IA : les pixels d’un clip importé existent déjà, il répond donc 422 editor_segment_references_unsupported. Une fois que la scène A une prise, ces deux écritures renvoient 409 editor_segment_references_locked — les images sont l’entrée qui a produit ses pixels, donc réécrire la colonne ferait revendiquer à la ligne une identité que le média n’a jamais eue. Préparez plutôt le changement : references/presign est toujours autorisé (il n’écrit rien), faites le PUT de l’image, puis envoyez generation_options.reference_s3_keys sur regenerate, et le nouvel ensemble arrive sur la nouvelle ligne de version. references/from-asset se limite alors à la préparation et renvoie staged: true avec le s3_key de l’asset et le segment inchangé, sans toucher à la colonne — et il refuse toujours un ensemble préparé qui ne tiendrait pas dans le modèle, avec le même 422 editor_reference_images_exceed_model_max. Dans generation_options, la clé suit la règle « absente = héritée » : l’omettre conserve les références enregistrées de la scène, un [] explicite les efface, chaque entrée doit être une clé créée par le references/presign de ce projet ou le s3_key d’une image du catalogue prête qui vous appartient (tout le reste donne 422 editor_reference_image_not_owned, car chaque clé préparée est présignée), et le même plafond par modèle s’applique. Une référence enregistrée dont l’objet a disparu depuis fait échouer la génération par sécurité avec 422 segment_reference_required, plutôt que de produire discrètement un sosie. Un PATCH du modèle d’une scène est soumis au même plafond : un changement qui laisserait plus de références que le reference_images_max du nouveau modèle est refusé avec 422 editor_reference_images_exceed_model_max et n’écrit rien. Enfin, apply-scenario RÉÉCRIT la timeline : ses nouvelles scènes n’ont aucune reference_s3_keys — réassociez les images d’identité après avoir appliqué un scénario.
variants explore la MÊME scène de N façons à la fois, au lieu d’une à la fois. Envoyez count (2–4), le modèle et les deux barrières : max_cost doit être exactement égal à count × le prix par prise (entier en unités mineures), et generation_quote_fingerprint prend la forme en six parties segment_id:model:variants:count:duration_seconds:pricing_version. Le prix par prise est le prix de génération tant que la scène n’a pas de prise terminée, et le prix de régénération dès qu’elle en a une. Seule la scène frontière peut être explorée (409 scene_not_frontier / scene_locked). Chaque prise est préparée comme version candidate de la même scène — la timeline n’est pas touchée — puis le run se met en attente needs_input, available_actions proposant variant_select et blockers[].details.candidate_segment_ids listant les prises terminées. Répondez-y sur POST /api/editor/:slug/generation/input avec le corps protégé habituel et input {"segment_id": <id de la prise choisie>} ; le corps ne nomme jamais l’action. La prise choisie devient la scène, les autres restent dans son historique de prises avec médias et prompts intacts (accessibles via la route versions/select). Une prise qui échoue est remboursée individuellement et n’apparaît simplement pas parmi les choix ; si toutes les prises échouent, le run échoue et rien n’a changé sur la timeline. Annulez un lot en cours avec POST /api/editor/:slug/generation/variants/cancel {generation_run_id, variant_batch_id} — l’id du lot écarte un éventuel lot suivant lancé sur la même scène. L’annulation ne rembourse que les prises pas encore transmises au fournisseur : une prise déjà en génération a été facturée par tentative, elle va donc jusqu’au bout et est conservée comme prise remplacée ordinaire dans l’historique de la scène, média et prompt intacts.
Finaliser — brouillon → final
Un modèle dont l’entrée video_models porte le tier "draft" (minimax-h3-max-turbo, veo3-lite, seedance-2.0-fast) est le niveau d’exploration le moins cher : tournez-y la scène, regardez-la, puis reportez UNE SEULE FOIS la prise retenue sur un modèle standard ou premium avec POST /api/editor/:slug/segments/:id/finalize. Le prompt de la prise brouillon, ses reference_s3_keys d’identité, sa durée définie et son réglage de dialogue (native_audio) sont copiés tels quels — le modèle est la SEULE variable — et tout ce que vous passez dans generation_options remplace la valeur copiée pour ce seul champ. Mécaniquement, c’est une régénération : une prise candidate qui ne fait qu’avancer, prévisualisée et vérifiée, la prise existante restant sélectionnée jusqu’à ce que la nouvelle soit validée, et la réponse est exactement celle de regenerate (un EditorSegment nu sous data, portant generation_summary). Le workflow est editor.scene.finalize@1, et cancel et retry fonctionnent exactement comme pour la régénération d’une scène — generation_summary.available_actions propose cancel_generation (POST .../segments/:id/cancel) et, en cas d’échec, retry_generation (POST .../segments/:id/retry).
Le finalize est facturé comme une GÉNÉRATION sur le modèle CIBLE, pas comme une régénération : une prise brouillon n’est pas une tentative ratée sur le modèle cible, c’est la toute première prise de la scène sur ce modèle. Le mot d’opération de l’empreinte est donc generation et le prix est le price.generation du modèle cible (ou sa ligne de price_table à la durée et à l’audio résolus). La forme est segment_id:model:generation:duration_seconds:pricing_version, où segment_id est l’id de la prise BROUILLON, model le modèle CIBLE, et duration_seconds la durée résolue — la durée définie/voice-first de la scène si elle en a une et que le modèle cible la prend en charge, sinon la valeur par défaut du modèle, 8 ; une scène de dialogue prend la forme en six parties …:duration_seconds:audio:pricing_version, exactement comme sur generate/regenerate. Les deux barrières sont obligatoires ici, contrairement à generate/regenerate unitaires où elles sont optionnelles, car un finalize change toujours de modèle — impossible de ne pas se prononcer sur le prix. La prise promue indique draft_of_segment_id (entier, nullable) dans le segment sérialisé, qui désigne la prise brouillon dont elle provient ; pour savoir si une prise EST un brouillon (et doit proposer Finalize), lisez tier dans l’entrée video_models de son modèle — draft_of_segment_id est le résultat d’une promotion, pas le marqueur d’un brouillon.
Deux refus sont propres à cette route, tous deux en 422 : editor_finalize_requires_draft_take quand le modèle de la prise actuelle n’est pas de niveau draft (une clé de modèle inconnue ou retirée compte aussi), avec des details indiquant model et tier ; et editor_finalize_target_tier_invalid quand le modèle cible demandé est lui-même de niveau draft, avec des details indiquant model et tier. Tout ce qu’elle peut renvoyer d’autre correspond à ce que renvoie regenerate : 402 balance_insufficient annonçant le prix de GÉNÉRATION, 409 generation_quote_changed, 409 client_upgrade_required (une requête qui envoie encore max_credits), 422 invalid_max_cost / invalid_generation_quote_fingerprint / editor_reference_images_exceed_model_max (vérifié par rapport à capabilities.reference_images_max du modèle CIBLE — elle refuse, elle ne supprime jamais silencieusement de références) / editor_reference_duration_unsupported (une promotion conditionnée par des références dont la durée reprise sort des reference_durations_seconds du modèle CIBLE) / editor_pacing_duration_mismatch / native_audio_*, 422 editor_segment_not_completed, 409 editor_segment_busy, et le refus d’admission 503. Le verrou d’admission de editor.scene.finalize@1 est fermé par défaut en production jusqu’à son ouverture : un déploiement tout neuf répond donc 503 generation_admission_closed.
Les 503 d’admission se présentent sous deux codes aux sémantiques de relance opposées. generation_admission_closed signifie que le workflow est désactivé administrativement : son verrou d’admission est un coupe-circuit opérateur propre à chaque workflow, qui ne se rouvre jamais tout seul, donc relancer ne servira à rien (details.retryable: false, details.operator_action_required: true, pas d’en-tête Retry-After) — signalez-le à un opérateur humain au lieu de relancer ou de changer discrètement de workflow ; details.admission_source distingue un verrou fermé par un opérateur ("database") d’un déploiement pas encore ouvert ("environment_default"). generation_admission_unavailable est une panne d’infrastructure transitoire (details.retryable: true, Retry-After: 30) — relancez la même requête plus tard. Les verrous sont limités à details.workflow_name : un verrou fermé sur une route ne dit rien des autres.
# The scene's current take was shot on a draft-tier model — its video_models entry says tier "draft"
# (minimax-h3-max-turbo, veo3-lite, seedance-2.0-fast). Explore there, then commit the take you kept ONCE onto a
# quality model. Priced as a GENERATION on the TARGET model: veo3 is 2.55 at the 8s default.
curl -X POST https://hubfluencer.com/api/editor/$SLUG/segments/81/finalize \
-H "authorization: Bearer $HF" -H "idempotency-key: finalize-seg:$SLUG:81" \
-H "content-type: application/json" \
-d '{"model":"veo3","max_cost":255,"generation_quote_fingerprint":"81:veo3:generation:8:2026-09-24-cost-v1"}'
# The response is regenerate's exactly: the EditorSegment under data — a candidate take carrying
# "draft_of_segment_id": 81
# plus a generation_summary for editor.scene.finalize@1. The draft take stays selected until the
# new one passes. Prompt, reference_s3_keys, authored duration and native_audio were copied verbatim.
# generation_options overrides ONE copied field — here, keep the model's own sound (4.95):
# {"model":"veo3","max_cost":495,
# "generation_quote_fingerprint":"81:veo3:generation:8:audio:2026-09-24-cost-v1",
# "generation_options":{"native_audio":true}}
Les prompts des scènes sont contrôlés au regard de la politique de contenu à chaque point d’entrée de génération payante (generate, regenerate, variants, batch-generate et la facturation automatique de render) : un prompt non conforme renvoie 422 prompt_rejected (code CONTENT_COMPLIANCE) sans rien facturer ; si le contrôle est temporairement indisponible, l’appel renvoie 503 compliance_unavailable (fermeture par défaut, rien n’est facturé — réessayez sous peu).
Ne relancez une scène échouée gérée par le Kernel que lorsque generation_summary.available_actions propose retry_generation pour editor.scene.generate ou editor.scene.regenerate : annoncez le prix exact de cette action (un objet monétaire), obtenez l’accord, puis faites POST /api/editor/:slug/segments/:id/retry avec generation_run_id, generation_revision et step_key issus de la même action. Idempotency-Key est obligatoire. La tentative échouée chez le fournisseur a été remboursée ; l’étape relancée chez le fournisseur est un nouveau prélèvement, soumis au solde, au prix immuable figé dans le run. La route relance sur place exactement le run échoué et refuse les barrières run/revision/step/materialization périmées.
Annulez une scène en cours avec POST /api/editor/:slug/segments/:id/cancel (video:generate). Idempotency-Key est obligatoire ; ne la réutilisez que pour les relances réseau de cette commande précise, protégée par jeton. Envoyez {"expected_generation_claim_token": "<generation_claim_token de la scène en cours de traitement>"} pour qu’une commande retardée ne puisse pas annuler une tentative suivante. Le jeton est obligatoire et doit être une chaîne non vide. La commande arrête cette génération individuelle, écarte une complétion concurrente du fournisseur (le premier qui écrit l’emporte), rembourse la tentative payée de façon idempotente et renvoie la scène en failed avec error_code user_cancelled — répéter l’annulation avec le même jeton et la même clé renvoie cette scène. Une annulation remboursée ne prépaie pas la tentative suivante ; la génération suivante ou la relance à prix figé proposée est payée séparément. Les scènes d’un lot ou d’autopilot ne peuvent pas être annulées individuellement (409 batch_generation_active / autopilot_active — utilisez les commandes d’annulation du lot ou d’autopilot) ; 409 editor_segment_not_processing quand la scène n’a pas de génération individuelle active ; 409 editor_segment_cancel_stale quand le jeton vise une tentative plus ancienne ; 422 invalid_generation_claim_token quand le champ obligatoire est omis ou invalide ; 404 pour une factory ou un segment inconnus ; 503 editor_segment_cancel_failed si l’annulation n’a pas pu être soldée — relancez avec la même clé.
Ossature vocale — modes de rythme
Chaque projet editor a un mode de rythme qui fait autorité, servi comme pacing_mode dans le payload editor. "scene_first" est la valeur par défaut, utilisée par tous les projets existants : les durées des scènes font autorité et la voix off y est ajustée dans la plage de vitesse de lecture 0,80–1,25x de Forge — comportement inchangé. "voice_first" inverse le contrat : le script de narration est l’ossature du projet. La voix est générée EN PREMIER, avant toute scène, chaque ligne est mesurée, et la durée de chaque scène est dérivée de sa ligne. Une ligne de narration ne peut donc jamais déborder de sa scène ni chevaucher la ligne suivante, puisque la scène a été dimensionnée à partir de la ligne.
Changez de mode avec POST /api/editor/:slug/pacing, corps mode: "scene_first" ou "voice_first". C’est gratuit, cela nécessite video:generate et ne prend PAS d’Idempotency-Key. Le changement est refusé avec 409 tant qu’une génération détient le projet — un run planifié sous un contrat ne doit jamais se voir appliquer l’autre en cours de route. Passer À voice_first marque pacing_stale: true chaque scène qui n’a pas encore d’authored_duration_seconds ; passer À scene_first efface tous les indicateurs pacing_stale et CONSERVE les durées définies (elles restent une trace fidèle de ce qui a été demandé). La réponse est le payload ordinaire de la factory editor plus un objet pacing_impact portant marked_stale, cleared_stale et scenes_without_authored_duration. Un mode inconnu renvoie 422 editor_pacing_mode_invalid.
POST /api/editor/:slug/voice-plan planifie l’ossature — corps voice_id (requis), style?, tone? ; Idempotency-Key est OBLIGATOIRE ; video:generate. La requête lance le workflow editor.voice.plan@1 : narration.generate (sans frais, mais consomme le quota d’assists IA exactement comme l’endpoint de narration ordinaire), puis voice.generate (ElevenLabs with-timestamps), puis pacing.derive (gratuit). La voix est tarifée selon la durée de la timeline : 0,15 par tranche entamée de 1 000 caractères de la plus longue narration que les scènes parlées peuvent contenir (prices.voice dans la réponse editor indique le montant exact). Le projet doit être en rythme voice_first (sinon 422 editor_pacing_mode_required) et avoir au moins une scène qui ne garde pas son audio d’origine. Les scènes n’ont PAS besoin d’être déjà générées ; c’est tout l’intérêt de ce mode. Un 201 renvoie le payload de la factory editor, et la clé generation_summary est entièrement OMISE quand il n’y a rien à signaler — jamais présente avec la valeur null.
La dérivation est authored_duration_seconds = snap_up_to_model_grid(line_duration + 0.4s lead + 0.5s tail + continuity_head_trim). line_duration est la durée parlée MESURÉE, tirée des horodatages par caractère d’ElevenLabs. L’amorce et la fin valent par défaut 0,4 s et 0,5 s et sont réglables côté serveur pour chaque déploiement. continuity_head_trim vaut 0,2 s (6 images à 30 fps) pour chaque scène après la première dont use_previous_frame vaut true — le rendu coupe ces images de conditionnement au début de la scène, l’ossature les prévoit donc dès le départ. L’arrondi supérieur à la grille du modèle utilise capabilities.durations_seconds du modèle de la scène : Veo accepte 4/6/8, donc un besoin de 5,1 s devient une scène de 6 s et le surplus est un silence de fin prévu, jamais un mot coupé. La durée définie est PLAFONNÉE à la durée par défaut du catalogue (8 s) — pour CHAQUE scène, y compris avec un modèle sans grille de durées publiée — si bien qu’une ossature dérivée ne peut jamais coûter plus que le prix approuvé par l’utilisateur. Une ligne qui ne TIENT PAS sous ce plafond est refusée avant tout mouvement d’argent : voice-plan (et l’étape voix d’un Autopilot voice-first) renvoie 422 editor_narration_line_too_long avec details.scenes, qui nomme chaque scène en cause (segment_id, position, estimated_line_seconds, max_authored_seconds). Elle n’est ni coupée ni surpayée en silence — une scène générée au plafond alors que la piste de voix est posée sur toute sa durée mesurée produirait un canevas que l’image ne peut pas couvrir. Raccourcissez la ligne, ou répartissez-la sur plus de scènes, puis planifiez à nouveau. Les imports ne sont jamais redimensionnés par l’ossature : une scène importée est un intervalle fixe à sa durée mesurée (un import de 30 s reste de 30 s, qu’une narration soit dite dessus ou non), et une ligne trop longue pour y tenir est refusée par le même 422. Chaque durée définie est calée sur la grille d’images de 1/30 s du rendu — vers le haut pour une scène dérivée, vers le bas pour un import fixe — afin que la conversion secondes-images du rendu soit exacte.
Deux champs portent l’ossature à chaque lecture. Le payload de la factory editor gagne pacing_mode ("scene_first" ou "voice_first"). Chaque segment gagne authored_duration_seconds (float ou null) — ce qui a été DEMANDÉ pour la scène, la dérivation de l’ossature — tandis que duration_seconds reste ce que MESURE le média généré, et null signifie « durée par défaut du modèle » ; ainsi que pacing_stale (booléen), true quand la ligne de narration de cette scène a changé après la dérivation de sa durée définie. Une scène pacing_stale peut toujours être rendue ; il n’est simplement plus garanti qu’elle tienne dans sa ligne.
# 1. Make the narration script this project's spine (free, no Idempotency-Key)
curl -X POST https://hubfluencer.com/api/editor/$SLUG/pacing \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-d '{"mode":"voice_first"}'
# 200 OK — the ordinary editor payload plus what the switch did to the timeline:
# "pacing_mode": "voice_first",
# "pacing_impact": {"marked_stale": 5, "cleared_stale": 0, "scenes_without_authored_duration": 5}
# 2. Plan the spine — priced from the timeline length (prices.voice), Idempotency-Key REQUIRED.
# The scenes do NOT have to be generated yet: the voice is what sizes them.
curl -X POST https://hubfluencer.com/api/editor/$SLUG/voice-plan \
-H "authorization: Bearer $HF" -H "idempotency-key: voice-plan:$SLUG" \
-H "content-type: application/json" \
-d '{"voice_id":"rachel","tone":"warm"}'
# 3. Poll GET /api/editor/$SLUG. Every scene now carries what it was ASKED for:
# { "id": 81, "duration_seconds": null, "authored_duration_seconds": 6.0, "pacing_stale": false }
# 4. Generate at the derived duration. Omit generation_options and the AUTHORED
# duration is the priced duration — the fingerprint must carry it (6, not 8).
curl -X POST https://hubfluencer.com/api/editor/$SLUG/segments/81/generate \
-H "authorization: Bearer $HF" -H "idempotency-key: gen-seg:$SLUG:81" \
-H "content-type: application/json" \
-d '{"model":"veo3-fast","max_cost":105,"generation_quote_fingerprint":"81:veo3-fast:generation:6:2026-09-24-cost-v1"}'
Reprise des lots
Le corps de retry est {generation_run_id, generation_revision} ; celui de cancel est {generation_run_id}. Copiez ces champs depuis le generation_summary qui propose l’action. Des barrières absentes ou mal formées renvoient 422 batch_command_required, tandis que des barrières valides mais dépassées renvoient 409 generation_authority_changed. Pollez à nouveau et n’émettez une nouvelle commande avec une nouvelle Idempotency-Key que si le résumé actuel propose toujours l’action.
generation_summary.available_actions est la seule source de disponibilité de la relance et de l’annulation. batch_generation_status, batch_generation_run_id et batch_generation_failed_position sont des projections d’affichage produit ; elles n’autorisent aucune commande, ne fixent pas la cadence de polling et ne déterminent pas l’état terminal. Tant qu’une action de relance est proposée, le reste de la timeline reste verrouillé en modification, sauf la scène échouée identifiée par current_step. Un blocage pour solde insuffisant ne se résout que par l’action proposée par generation_summary, une fois que le titulaire du compte a ajouté des fonds ou que le périmètre a été réduit.
# Write your own scenario instead of generating it (free, no assist)
curl -X PATCH https://hubfluencer.com/api/editor/$SLUG/scenario \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-d '{"scenario_prompt":"Scene 1 … Scene 2 … Scene 3 …"}'
# Read GET /api/editor/$SLUG first, choose from data.video_models, and obtain approval for its exact price/notices
# Generate one scene with Veo 3.1 Fast (1.35 in the account currency at the 8s default, pricing version
# 2026-09-24-cost-v1); max_cost is integer minor units of the account currency; poll until completed
curl -X POST https://hubfluencer.com/api/editor/$SLUG/segments/81/generate \
-H "authorization: Bearer $HF" -H "idempotency-key: gen-seg:$SLUG:81" \
-H "content-type: application/json" -d '{"model":"veo3-fast","max_cost":135,"generation_quote_fingerprint":"81:veo3-fast:generation:8:2026-09-24-cost-v1"}'
# Author a non-default scene length: pick a value from the model's capabilities.durations_seconds
# and read its price from the model's price_table row for that duration_seconds and native_audio
curl -X POST https://hubfluencer.com/api/editor/$SLUG/segments/81/generate \
-H "authorization: Bearer $HF" -H "idempotency-key: gen-seg-12s:$SLUG:81" \
-H "content-type: application/json" -d '{"model":"kling-o3-pro","generation_options":{"duration_seconds":12},"max_cost":220,"generation_quote_fingerprint":"81:kling-o3-pro:generation:12:2026-09-24-cost-v1"}'
# Voice-over (0.15 per started 1,000 script characters), then render. This capless render with an empty fingerprint succeeds only after every scene is generated.
# If scenes are missing, GET the Editor again, quote the exact ordered cohort, obtain approval, and send that cap/fingerprint.
curl -X POST https://hubfluencer.com/api/editor/$SLUG/generate-voice \
-H "authorization: Bearer $HF" -H "content-type: application/json" -d '{"voice_id":"rachel"}'
curl -X POST https://hubfluencer.com/api/editor/$SLUG/render -H "authorization: Bearer $HF" -H "idempotency-key: render:$SLUG" -H "content-type: application/json" -d '{"generation_quote_fingerprint":""}'
Imports & assets locaux
Apportez vos propres médias dans un projet editor — vos propres rushes comme scènes, plus une image produit, une image de fin et un logo de marque. Tout cela est gratuit. Les imports suivent un flux presign → PUT vers le stockage → confirm ; le fichier est ensuite traité de façon asynchrone avant de pouvoir aller sur la timeline.
/api/editor/:slug/uploads/presign
free
Obtenir une URL PUT présignée pour importer une vidéo.
| Champ du corps | Type | Remarques |
|---|---|---|
filename requis |
string | Nom du fichier d’origine. |
mime_type requis |
string | video/mp4, video/quicktime, video/webm, video/x-matroska. |
size_bytes requis |
integer | ≤ 500 Mo par fichier ; vidéo ≤ 5 min. Compte aussi dans votre quota de stockage par utilisateur. |
fit_mode optionnel |
string | "cover" ou "blur" (par défaut). |
Renvoie 422 upload_quota_exceeded (avec quota.limit_bytes, used_bytes, pending_bytes, reserved_bytes et remaining_bytes) si l’import ferait dépasser la limite de votre stockage total d’imports actifs. Il n’existe pas de route de suppression par import : utilisez un fichier plus petit, attendez le nettoyage des imports abandonnés pending/failed, ou supprimez un projet editor dont vous n’avez plus besoin pour libérer ses imports.
200 OK
{
"data": {
"upload_id": 510,
"presigned_url": "https://r2…/editor/$SLUG/uploads/uuid.mp4?X-Amz-Signature=…",
"s3_key": "editor/$SLUG/uploads/uuid.mp4",
"expires_in_seconds": 3600
}
}
/api/editor/:slug/uploads/:upload_id/confirm
free
Confirmer le PUT. Le serveur valide l’objet (le Content-Type et la taille doivent correspondre au presign) et met le traitement en file d’attente.
/api/editor/:slug/uploads
video:read
Lister les imports avec leur statut de traitement. Pollez jusqu’à ce que status vaille "ready" avant de placer le clip.
200 OK — status flows pending → processing → ready | failed
{
"data": [
{ "id": 510, "filename": "clip.mp4", "mime_type": "video/mp4",
"status": "ready", "duration_seconds": 6.2, "width": 1080, "height": 1920,
"error_message": null, "attached_segment_ids": [83] }
]
}
/api/editor/:slug/segments/from-upload
free
Ajouter un import prêt à la timeline comme scène terminée. Corps : upload_id. Refuse editor_upload_not_ready, batch_generation_active, autopilot_active (pendant qu’Autopilot tourne), editor_segment_limit (20 scènes max).
/api/editor/:slug/segments/:id/use-asset
free
Définir la vidéo d’une scène à partir d’un asset existant. Corps : exactement un champ parmi upload_id et source_segment_id. Réutilisez un clip dans plusieurs scènes, ou remplacez une scène générée par vos propres images.
Les data du 200 sont le segment mis à jour plus un objet impact décrivant les répercussions en aval sur la continuité — remplacer une scène change l’image dont hérite la scène IA suivante avec continuité activée. Clés : requires_regeneration (bool), stale_video_segment_ids / stale_video_positions (scènes IA terminées suivantes, désormais obsolètes), affected_segment_ids / affected_positions (toute la chaîne en aval), anchor_segment_id / anchor_position, stop_reason / stop_position, et action ("segment_asset_replace"). Quand requires_regeneration vaut true, régénérez les scènes listées avant le rendu — render refuse une timeline obsolète.
Les gros fichiers passent par un flux multipart avec reprise (à utiliser dès ≥ 50 Mio / 52 428 800 octets, parties de 8 Mio) :
POST …/uploads/multipart/init, …/sign-part, …/complete, …/abort. init renvoie part_size + parts_count ; signez chaque partie, envoyez ses octets en PUT, conservez l’ETag, puis envoyez la liste ordonnée des parties à complete (abort en cas d’échec).
Images — produit, image de fin, logo
Même schéma presign → PUT → confirm (image/jpeg ou image/png). Les images produit sont plafonnées à 8 Mio, les images de fin à 20 Mio et les logos à 5 Mio. Transmettez tel quel le s3_key de presign à confirm.
Les anciennes routes de compatibilité ont été entièrement supprimées — POST product/apply-default-placement, scene-asset presign/confirm/impact et reference-images/impact n’existent plus dans le routeur et renvoient désormais un simple 404. Seules subsistent les barrières au niveau du corps sur l’ajout et la mise à jour de segment : product_placement ou product_excluded renvoie 410 product_placement_removed, scene_asset_role / scene_asset_prompt_adaptation_enabled / scene_asset_s3_key renvoie 410 scene_assets_disabled, et tout autre champ inconnu renvoie 422 unsupported_field. Rédigez le contenu des scènes via prompt et use_previous_frame.
Flux complet
# 1. Presign (Content-Type MUST equal the mime_type you send here)
curl -X POST https://hubfluencer.com/api/editor/$SLUG/uploads/presign \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-d '{"filename":"clip.mp4","mime_type":"video/mp4","size_bytes":4821004}'
# 2. PUT the bytes to presigned_url with the SAME Content-Type
curl -X PUT "$PRESIGNED_URL" -H "content-type: video/mp4" --data-binary @clip.mp4
# 3. Confirm — queues processing (metadata + frame extraction)
curl -X POST https://hubfluencer.com/api/editor/$SLUG/uploads/510/confirm -H "authorization: Bearer $HF"
# 4. Poll GET /api/editor/$SLUG/uploads until the row's status is "ready"
# 5. Drop it on the timeline as a finished scene
curl -X POST https://hubfluencer.com/api/editor/$SLUG/segments/from-upload \
-H "authorization: Bearer $HF" -H "content-type: application/json" -d '{"upload_id":510}'
Rendus
/api/editor/:slug/renders
video:read
Chaque version de rendu avec status, video_url/thumbnail_url présignées, retryable, combination_hash et is_stale. Le statut vaut pending, processing, completed ou failed. Utilisez-la pour récupérer l’URL d’un rendu terminé ou trouver un rendu échoué à relancer ; is_stale à null signifie « inconnu », pas « à jour ».
200 OK
{
"data": [
{ "id": 9, "version": 2, "status": "completed",
"video_url": "https://…/render.mp4?X-Amz-Expires=86400",
"thumbnail_url": "https://…/render.jpg?X-Amz-Expires=86400",
"duration_seconds": 18.0, "error_message": null, "retryable": false,
"combination_hash": "v2:…", "is_stale": false,
"inserted_at": "2026-06-03T10:20:00Z" },
{ "id": 8, "version": 1, "status": "failed",
"video_url": null, "error_message": "forge_timeout", "retryable": true }
]
}
/api/editor/:slug/renders/:id/retry
free
Relancer un rendu échoué depuis son instantané sauvegardé. Renvoie 202 avec data.video_result_id, status et is_duplicate ; pollez l’état de l’editor jusqu’à la fin. Seuls les rendus échoués peuvent être relancés (editor_render_not_retryable sinon).
Transparence IA & conformité (AI Act de l’UE, art. 50)
Chaque rendu — pub editor, short et diapositive de carrousel — porte des Content Credentials C2PA (un marquage signé, lisible par machine, « ceci est généré par IA », avec la liste des modèles générateurs) ET un badge visible de transparence IA ACTIVÉ par défaut. Les plateformes de diffusion (TikTok, Meta, YouTube) lisent le manifeste C2PA à l’import et peuvent en déduire automatiquement leur propre label IA — désactiver le badge n’empêche donc PAS les labels des plateformes. Publier la vidéo fait de VOUS le déployeur au sens de l’art. 50(4) : l’évaluation de la mention vous revient, c’est pourquoi désactiver le badge exige un acquittement explicite, horodaté et journalisé.
Les réglages du badge se trouvent sur le projet et se définissent via les endpoints de mise à jour habituels (PATCH /api/editor/:slug et PATCH /api/shorts/:slug) : ai_disclosure_mode ("generated" par défaut | "modified" | "none") et ai_disclosure_position ("auto" par défaut — le coin opposé à votre logo, dans la zone sûre de la plateforme — ou un coin explicite). Passer le mode à "none" sans ai_disclosure_ack: true dans la même requête renvoie 422 ai_disclosure_ack_required. Une désactivation réussie horodate ai_disclosure_ack_at / ai_disclosure_ack_by_user_id (renvoyés sur le projet et dans le dossier de conformité) et écrit une entrée dans le journal d’audit ; réactiver le badge n’efface jamais cet horodatage historique. Les carrousels sont toujours entièrement générés par IA et toujours marqués + étiquetés (pas de bascule par slider).
// Turning the label off is an explicit, acknowledged decision:
PATCH /api/editor/:slug (or /api/shorts/:slug)
{ "ai_disclosure_mode": "none", "ai_disclosure_ack": true }
// Without the ack in the SAME request:
422 { "error": "ai_disclosure_ack_required", "message": "…" }
/api/video-factories/:slug/compliance
video:read
Dossier de preuves au titre de l’article 50, prêt pour un audit, pour un projet (fonctionne avec les slugs editor comme short) : le type de projet, l’état de la mention avec son horodatage d’acquittement, les éléments de provenance (si la timeline actuelle contient des images de l’utilisateur, la liste des modèles générateurs et le marquage lisible par machine réellement appliqué à ce type — "c2pa" pour l’editor et le short rendus par Forge, "none" pour un projet de tracking, qui est votre propre clip avec une surcouche de vision par ordinateur incrustée et n’est jamais signé), ainsi que les rendus terminés. Transmettez-le tel quel à un client d’agence ou en réponse à une demande d’information d’un régulateur.
200 OK
{
"data": {
"kind": "editor",
"ai_disclosure": {
"mode": "generated", "position": "auto",
"ack_at": null, "ack_by_user_id": null
},
"provenance": {
"contains_user_footage": false,
"models": ["veo3", "elevenlabs-tts", "elevenlabs-music"],
"machine_readable_marking": "c2pa"
},
"renders": [
{ "video_result_id": 42, "status": "completed",
"generated_at": "2026-08-01T12:00:00Z" }
]
}
}
Sources produit
Le point de départ de la boucle « déléguer mon contenu » : transformer une page produit en informations et en images. Chaque récupération se fait CÔTÉ SERVEUR, protégée contre le SSRF (https uniquement, aucune redirection vers des plages internes). L’extraction est sans état ; import stocke un asset du catalogue. Les trois sont gratuits, nécessitent video:generate et sont limités à 30/min.
/api/product-sources/extract
free
video:generate
Récupérer une page produit publique (HTML uniquement) et renvoyer les informations extraites : nom, description, bénéfices, prix, marque, disponibilité, plus des URL d’images produit candidates et une URL de logo du site ou de la marque. URL uniquement — les images ne sont PAS téléchargées ici. Corps : {url} (http(s) absolue, ≤2048 caractères). 422 unsupported_content_type pour une page non HTML, too_large pour une page trop volumineuse.
/api/product-sources/preview-image
free
video:generate
Récupérer une image depuis une URL publique (JPEG/PNG, vérifiée par détection du magic number — pas seulement du Content-Type — et réduite à un petit aperçu) et renvoyer une data URL inline pour relecture. N’écrit rien, ne stocke aucun asset. Corps : {image_url}. 422 pour une URL non sûre ou qui ne pointe pas vers une image.
/api/product-sources/import-image
free
video:generate
Récupérer une image depuis une URL publique (JPEG/PNG vérifié par magic number, taille plafonnée à la limite des images du catalogue) et la stocker comme CatalogAsset prêt. Corps : {image_url, product_profile_id?, link_as?}. Avec product_profile_id, l’asset est lié à ce profil comme link_as "primary_image" (par défaut) ou "logo". 404 pour un profil inconnu ou appartenant à un autre compte ; 422 pour une URL non sûre ou qui ne pointe pas vers une image.
Profils produit & de marque
Une identité réutilisable, propre à l’utilisateur, dont chaque brouillon de campagne hérite au lieu de redécrire la marque. Un profil produit ancre l’IA (bénéfices, offre, preuves, audience, CTA, conformité) ; un profil de marque oriente le ton et le format et porte le logo. Simple CRUD ; les lectures nécessitent video:read, les écritures video:generate ; tout est gratuit. Seul name est requis sur chacun.
/api/product-profiles
video:read
Lister / lire vos profils produit. GET /api/product-profiles/:id en renvoie un. Passez un id à plan / hook-variations / à la création de campaign-pack pour que la campagne soit planifiée à partir d’une source de vérité enregistrée.
/api/product-profiles
free
video:generate
Créer une fiche produit. Corps : {name (requis), description?, benefits?[], offer?, proof_points?[], audience?, cta?, banned_claims?[], brand_tone?, source_url?}. PATCH /api/product-profiles/:id modifie n’importe lequel de ces champs (gratuit) ; DELETE /api/product-profiles/:id la supprime (204). Associez des images via product-sources/import-image. 404 pour un id inconnu ou appartenant à un autre compte.
/api/brand-profiles
video:read
Lister / lire vos profils de marque (GET /api/brand-profiles/:id pour un seul). Passez un id comme brand_profile_id à POST /api/shorts / hook-variations / la création de campaign-pack / series pour que les brouillons héritent de la marque et du logo.
/api/brand-profiles
free
video:generate
Créer une identité de marque. Corps : {name (requis), logo_s3_key?, primary_color?, secondary_color?, font_family?, tone_words?[], default_cta?, default_audience?, banned_claims?[], preferred_visual_language?, preferred_creative_format?}. Les couleurs sont en hexadécimal (#09EFBE) ; preferred_visual_language / preferred_creative_format acceptent les mêmes listes de valeurs que Shorts/editor. PATCH /api/brand-profiles/:id modifie n’importe quel champ ; DELETE le supprime (204).
/api/brand-profiles/:id/assets
free
video:generate
Associer l’un de vos assets du catalogue à un profil de marque. Corps : {catalog_asset_id, role} où role vaut logo | product_image | other. DELETE /api/brand-profiles/:id/assets/:asset_id supprime un lien — :asset_id est l’id du LIEN (le assets[].id du profil), pas le catalog_asset_id. 422 si l’asset du catalogue ne vous appartient pas ; 404 pour un profil inconnu ou appartenant à un autre compte.
Packs de campagne
Le conteneur de planification : planifiez une campagne, enregistrez-la comme pack et matérialisez chaque élément en BROUILLON modifiable (un projet vidéo editor/short ou un carrousel d’images). La planification (plan + hook-variations) ne génère aucun média et consomme le quota GRATUIT d’assists IA — rien n’est prélevé sur le solde. Enregistrer un pack et matérialiser des éléments est gratuit. Générer/rendre chaque brouillon est une étape DISTINCTE, explicite et PAYANTE (autopilot / generate). Les lectures nécessitent video:read, les écritures video:generate.
/api/campaign-packs/plan
free
video:generate
Transformer des informations produit en PLAN de campagne : accroches, formats recommandés, une phrase de positionnement et une ESTIMATION par élément (objets monétaires dans la devise du compte). N’enregistre rien. Corps : {product_profile_id | product{name,...}, goal?, platform?, language?}. Gratuit, mais consomme 1 assist IA gratuit (429 ai_assist_quota_exceeded quand le quota quotidien est épuisé). Limité à 30/min.
/api/campaign-packs/hook-variations
free
video:generate
Transformer des informations produit + un brand_profile_id optionnel en 3 à 6 variations d’accroche distinctes, chacune avec une accroche, un angle, un format de sortie suggéré (editor_ad/short/carousel), un script de brouillon, une légende, des hashtags et une estimation par variation (un total et des prix par ligne sous forme d’objets monétaires). N’enregistre rien. Corps : {product_profile_id | product{...}, brand_profile_id?, count? 3..6, goal?, platform?, language?}. Gratuit, 1 assist IA gratuit. Limité à 30/min.
/api/campaign-packs
free
video:generate
Enregistrer un pack et ses éventuels éléments TEXTE inline (accroche/légende). Les éléments editor_ad/short/carousel s’ajoutent via /variations (ci-dessous), pas ici. Corps : {title?, product_profile_id?, brand_profile_id?, brief?, source_type?, source_url?, language?, items?:[{kind:"hook"|"caption", ...}]}. Liez un profil produit ou de marque pour que les brouillons matérialisés héritent de l’image et du logo. GET /api/campaign-packs liste vos packs ; GET /api/campaign-packs/:id en renvoie un avec ses éléments.
/api/campaign-packs/:id/variations
free
video:generate
Enregistrer les variations d’accroche sélectionnées comme éléments du pack et, par défaut (materialize:true), transformer chacune en BROUILLON. Corps : {variations:[{suggested_format:"editor_ad"|"short"|"carousel", hook?, angle?, draft_script?, caption?, hashtags?, estimate? (entier en unités mineures — le total estimé de la variation)}], materialize?}. 12 variations au maximum par appel ; limité à 15/min. Traitement au mieux, variation par variation : celle qui ne peut pas être matérialisée reste un élément "planned" avec une raison enregistrée, tandis que les autres sont quand même matérialisées — le résumé de la réponse liste created/materialized/planned + skipped_reasons.
/api/campaign-packs/:id/items/:item_id/materialize
free
video:generate
Matérialiser un élément planifié editor_ad/short/carousel en BROUILLON et le relier à l’élément. Idempotent — rappeler un élément qui vient d’être matérialisé renvoie le brouillon existant (pas de doublon). Limité à 60/min. 422 pour un élément accroche/légende, un élément déjà matérialisé, ou quand vous atteignez une limite de brouillons. Chaque élément porte son statut (planned → draft → generating → completed | failed), le slug du brouillon + la route de lien profond (editor_ad/short → le projet vidéo, carousel → le slider) et son estimation. Générez ensuite chaque brouillon EXPLICITEMENT : POST /api/editor/:slug/autopilot pour les vidéos, POST /api/sliders/:slug/generate pour les carrousels.
Séries & épisodes
Une série est un modèle d’émission réutilisable pour du contenu récurrent (« Mythe ou réalité », « Le conseil du fondateur »), lié à un profil de marque et à un profil produit facultatifs. Le planificateur rédige des IDÉES d’épisodes à venir (quota gratuit d’assists IA) ; materialize transforme une idée en brouillon gratuit, regroupé sous le pack de campagne glissant de la série. Les lectures nécessitent video:read, les écritures video:generate ; tout est gratuit.
/api/series
video:read
Lister / lire vos séries (GET /api/series/:id pour une seule). GET /api/series/:id/dashboard renvoie la série et ses épisodes regroupés en colonnes — à venir (idées), en brouillon, publiés — avec des notes de performance par élément.
/api/series
free
video:generate
Créer un modèle d’émission. Corps : {name (requis), template (requis), brand_profile_id?, product_profile_id?, cadence?, tone?, default_format?, status?}. default_format vaut editor_ad/short/carousel ; status bascule entre active/paused/archived (une série en pause ne propose plus de nouvelles idées). Un profil de marque ou produit fourni doit vous appartenir (422 invalid_brand_profile / invalid_product_profile). PATCH /api/series/:id modifie n’importe quel champ ; DELETE la supprime (204).
/api/series/:id/episodes/plan
free
video:generate
Rédiger 3 à 5 idées d’épisodes à venir (status "idea") pour la série, ancrées dans son profil produit et orientées par sa marque, son ton et sa cadence. Corps : {count? 3..5, language?}. Les enregistre comme épisodes. Gratuit, mais consomme 1 assist IA gratuit (429 quand le quota est épuisé). Limité à 30/min. GET /api/series/:id/episodes liste les épisodes (filtre ?status optionnel).
/api/series/:id/episodes/:episode_id/materialize
free
video:generate
Transformer un épisode "idea" en BROUILLON gratuit et modifiable dans le pack glissant de la série, qui hérite de la marque de la série. Idempotent — rappeler un épisode qui vient d’être matérialisé renvoie le brouillon existant. Limité à 60/min. 422 pour un épisode déjà matérialisé ou qui n’est plus au stade idea, ou pour une limite de brouillons. L’épisode porte le slug de son brouillon + sa route ; générez-le ensuite explicitement.
/api/series/:id/episodes/:episode_id/mark-posted
free
video:generate
Indiquer que vous avez publié manuellement un épisode EN BROUILLON (drafted → published, horodate posted_at) — il n’existe pas d’API de publication sur les plateformes. Idempotent : marquer à nouveau un épisode publié renvoie 200 avec le posted_at d’origine. 422 invalid_status pour un épisode idea/archived (matérialisez-le d’abord) ; 404 pour une série ou un épisode inconnus ou appartenant à un autre compte.
Performance
Associez des relevés de performance légers + des étiquettes d’attributs créatifs à un ÉLÉMENT de campaign-pack (item_id depuis GET /api/campaign-packs/:id), puis lisez un agrégat PRUDENT sur vos propres éléments. Les lectures nécessitent video:read, les écritures video:generate ; tout est gratuit (seul make-more consomme le quota gratuit d’assists IA).
/api/campaign-pack-items/:item_id/performance
free
video:generate
Enregistrer un relevé de performance. Corps : {platform, views?, likes?, comments?, shares?, clicks?, conversions?, revenue?, watch_time_seconds?, source?, captured_at?, metadata?}. Ajout seul — plusieurs relevés par élément (captured_at, qui vaut maintenant par défaut, les distingue), SANS clé d’idempotence : ne relancez donc pas à l’aveugle. Limité à 30/min. GET /api/campaign-pack-items/:item_id/performance les liste (du plus récent au plus ancien) avec un petit résumé. 404 pour un élément inconnu ou appartenant à un autre compte.
/api/campaign-pack-items/:item_id/attributes
free
video:generate
Créer ou mettre à jour l’UNIQUE ligne d’attributs créatifs que la boucle de recommandation met en corrélation avec la performance. Corps : {hook_type?, creative_format?, visual_language?, product_category?, cta?, length?, caption_style?, metadata?}. Upsert atomique — un nouvel appel met à jour la même ligne. GET /api/campaign-pack-items/:item_id/attributes renvoie la ligne ou null. 404 pour un élément inconnu ou appartenant à un autre compte.
/api/performance/recommendations
video:read
Agréger vos éléments qui ont À LA FOIS des attributs ET ≥1 relevé en une recommandation prudente : le meilleur hook_type / creative_format / visual_language selon les vues moyennes, vos meilleurs éléments et un niveau de confiance HONNÊTE ("none" / "low" / "moderate", fonction de la taille de l’échantillon), plus une note non causale. Elle n’affirme JAMAIS de causalité, de ROAS ni d’attribution — ce sont des corrélations dans vos propres données, une piste à tester. Présentez la note et la confiance telles quelles. Pas de LLM, pas de quota, aucun coût.
/api/campaign-pack-items/:item_id/make-more
free
video:generate
Générer 3 variations d’accroche de suite à partir d’un élément gagnant, en reprenant son angle, son format et son accroche tout en conservant son profil de marque et son profil produit. Gratuit, mais consomme 1 assist IA gratuit (429 quand le quota est épuisé). Limité à 30/min. 404 pour un élément inconnu ou appartenant à un autre compte ; 422 quand l’élément n’a pas de contexte produit exploitable. Passez une variation choisie à /variations pour en faire un brouillon.
Liens de relecture
Créez un lien de relecture à durée limitée, à jeton haché, pour UN résultat terminé, afin qu’un relecteur ouvre /review/:token sans compte. Le jeton en clair + l’URL ne sont renvoyés qu’UNE SEULE FOIS (seul le hash est stocké) — affichez-les immédiatement. Les lectures nécessitent video:read, les écritures video:generate ; tout est gratuit.
/api/review-links
free
video:generate
Créer un lien de relecture pour exactement un de vos résultats TERMINÉS. Corps : {video_result_id | slider_id (un seul des deux), campaign_pack_id?, expires_in_hours?, download_enabled?, version_label?}. expires_in_hours vaut 168 par défaut (7 jours) et est ramené dans une plage prise en charge. 422 pour un résultat non terminé ou appartenant à un autre compte, ou pour un pack de campagne d’un autre compte. Renvoie {id, token, review_url, expires_at, ...} — le jeton n’apparaît qu’UNE SEULE FOIS.
/api/review-links
video:read
Lister vos liens de relecture (du plus récent au plus ancien) avec leur statut (pending → viewed → approved | changes_requested | revoked) et un comment_count visible. Ne renvoie jamais le jeton. GET /api/review-links/:id en renvoie un. DELETE /api/review-links/:id révoque un lien (status → "revoked" ; l’URL cesse de fonctionner immédiatement).
Catalogue d’assets
Votre bibliothèque d’assets réutilisables (images + vidéos). Un asset importé ici peut être réutilisé d’un projet à l’autre via les routes from-asset des shorts / sliders / editor / tracking. Les lectures nécessitent video:read, les écritures video:generate ; tout est gratuit. La bibliothèque est ouverte à tout compte connecté.
/api/assets
video:read
Lister votre catalogue (du plus récent au plus ancien), chaque entrée avec media_type, mime, size, description, status et une asset_url présignée ; meta.quota contient votre quota de stockage. ?limit / ?offset optionnels (offset = page * limit). GET /api/assets/quota renvoie seulement le quota (octets limit / used / pending / reserved / remaining).
/api/assets/presign
free
video:generate
Présigner un import. Corps : {filename, mime_type, size_bytes} → { data: {asset_id, presigned_url, s3_key} }. Envoyez les octets bruts en PUT sur presigned_url avec l’en-tête Content-Type: <le mime présigné>, puis POST /api/assets/:id/confirm {duration_seconds?, width?, height?, description?} — confirm fait un HEAD sur l’objet et valide taille/type/durée (422 en cas de non-correspondance). Images ≤ 20 Mio ; vidéos ≤ 500 Mo, avec duration_seconds ≤ 60 OBLIGATOIRE. Limité à 30/min.
Publications programmées (calendrier éditorial)
Programmer un rendu TERMINÉ pour une date de publication future. Une programmation en rappel seul (sans compte social associé) envoie une notification push à l’heure prévue — vous publiez manuellement, puis bouclez la boucle avec mark-posted. Un jeton d’agent (video:generate) peut créer, reprogrammer, annuler et marquer ces programmations en rappel seul, et donc piloter tout le calendrier éditorial. Associer un compte social transforme une programmation en PUBLICATION AUTOMATIQUE : cela exige en plus account:admin et se fait uniquement dans l’app (en étant connecté). Les lectures nécessitent video:read.
/api/scheduled-posts
video:read
Lister vos publications programmées, triées par scheduled_at croissant. Paramètres de requête optionnels : ?from= / ?to= (bornes ISO-8601 UTC sur scheduled_at) et ?status= (scheduled | reminded | posted | publishing | published | failed | canceled).
/api/scheduled-posts/:id
video:read
Récupérer l’une de vos publications programmées. 404 pour des id inconnus ou appartenant à un autre compte.
/api/scheduled-posts
free
video:generate
Programmer l’un de vos rendus TERMINÉS pour un instant UTC futur (scheduled_at) sur une plateforme cible. SANS id de compte social, c’est une programmation en rappel seul — un jeton d’agent (video:generate) peut la créer. AVEC un id de compte (tiktok_account_id pour "tiktok", instagram_account_id pour "instagram", l’un de vos propres comptes), c’est une PUBLICATION AUTOMATIQUE qui exige en plus account:admin (sinon 403 insufficient_scope). Peut être regroupée sous un élément de pack de campagne. N’enregistre que la programmation ; ne facture rien. 500 publications en attente au maximum par utilisateur (au-delà, 422 scheduled_post_limit_reached).
/api/scheduled-posts/:id
free
video:generate
Reprogrammer une publication "scheduled" ou "reminded" à un nouveau scheduled_at FUTUR (une publication reminded est réarmée en "scheduled"). video:generate gère une publication en rappel seul ; une publication liée à un compte social (ou une mise à jour qui en associe un) exige en plus account:admin (403 insufficient_scope). 422 not_reschedulable / scheduled_in_past / invalid_scheduled_at ; 404 pour des id inconnus ou appartenant à un autre compte.
/api/scheduled-posts/:id
free
video:generate
Annuler une publication "scheduled", "reminded" ou "failed" (status -> "canceled"). video:generate annule une publication en rappel seul ; annuler une publication liée à un compte social exige en plus account:admin (403 insufficient_scope). 422 not_cancelable pour une publication déjà posted/publishing/published/canceled ; 404 pour des id inconnus ou appartenant à un autre compte.
/api/scheduled-posts/:id/mark-posted
free
video:generate
Confirmer que vous avez publié ce contenu manuellement : fait passer une publication "scheduled" ou "reminded" à "posted" et horodate posted_at. Atomique — une publication dans tout autre statut (y compris déjà publiée, donc un second appel) renvoie 422 not_markable. 404 pour des id inconnus ou appartenant à un autre compte. N’appelle aucune API de plateforme ; ne facture rien. Permet à un agent de boucler la boucle du rappel après la publication.
Assists IA
Les appels d’assistance IA puisent dans un quota quotidien gratuit de 20, à l’échelle du compte — distinct du solde. Cela couvre tous les endpoints de rédaction de contenu : generate-scenario, generate-narration, enhance-prompt et suggest-* (music-prompt / next-scene) de l’editor ; text/generate des shorts ; plan et hook-variations des campaign-packs ; episodes/plan des séries ; et make-more. Une fois le quota épuisé, vous pouvez soit en débloquer davantage (0,45 → +10), soit rédiger le contenu vous-même avec les endpoints PATCH gratuits.
/api/ai-assists
video:read
Le quota actuel.
200 OK
{
"data": {
"used": 4, "limit": 20, "bonus": 0, "remaining": 16,
"resets_at": "2026-06-04T00:00:00Z",
"unlock_cost": { "amount_minor": 45, "currency": "EUR" }, "unlock_batch_size": 10
}
}
/api/ai-assists/unlock
0.45
video:generate
Dépensez 0,45 pour +10 assists. C’est un achat renouvelable — n’envoyez PAS d’Idempotency-Key fixe (une clé réutilisée rejoue le premier déblocage pendant 24 h et ignore silencieusement les achats suivants).
Erreurs, limites de débit & polling
402
402 Payment Required
{ "error": "balance_insufficient", "message": "Your balance is too low.",
"required": { "amount_minor": 160, "currency": "EUR" },
"available": { "amount_minor": 100, "currency": "EUR" } }
429
429 Too Many Requests
{ "error": "ai_assist_quota_exceeded", "remaining": 0,
"can_unlock": true, "unlock_cost": { "amount_minor": 45, "currency": "EUR" }, "unlock_batch_size": 10 }
Limites de débit
Lorsqu’une route est limitée en débit, le plafond par utilisateur figure ci-dessous ; les requêtes qui le dépassent sont rejetées — patientez une minute et réessayez. Tout le reste n’est limité que par le solde du compte et le quota d’assists IA.