Référence de l'API
Base : https://app.durum.ai/api/v1/ Authentification : en-tête Authorization: Bearer durum_sk_live_… (voir Authentification).
Conventions
- Lecture : méthode
GET, paramètres dans la chaîne de requête.fromettosont des datesYYYY-MM-DD, optionnelles sauf mention contraire. Sans période, la route applique sa propre fenêtre par défaut. - Écriture : méthode
PUTouPOSTselon la route, corps JSON avecContent-Type: application/json. Chaque écriture exige un scopewrite:*. - Réponse :
200avec{ "data": … }. Toute erreur renvoie{ "error": "…" }avec le code HTTP correspondant. Les réponses portentCache-Control: private, no-store. - Vos données seulement : le compte est déduit de la clé. Aucun paramètre ne permet d'en désigner un autre.
- Champs retirés : voir Champs jamais renvoyés.
Index auto-documenté
GET https://app.durum.ai/api/v1 (avec ou sans barre oblique finale) renvoie sans authentification la liste des routes de lecture et d'écriture avec leur méthode, leur scope et leur chemin complet (/api/v1/dashboard/kpis), ainsi que base_url et le lien vers cette page.
GET https://app.durum.ai/api/v1/tools (clé requise) renvoie un manifeste JSON (tools[] avec name, method, path, scope, inputSchema) limité aux routes couvertes par les scopes de votre clé. Il ne recense pas toutes les routes (les routes d'appels et de créatifs en sont absentes) : l'index ci-dessus reste la liste de référence.
Dashboard
| Route | Scope | Paramètres |
|---|---|---|
GET /dashboard/kpis | read:dashboard | from, to |
GET /dashboard/daily | read:dashboard | from, to |
GET /dashboard/breakdowns | read:dashboard | from, to, dim (défaut source ; valeurs source, campaign, product, rep) |
Réponses :
kpis: un objet de KPI agrégés sur la période (notammentleads,applications,bookings,meetings,shows,no_shows,total_sales,revenue,ticket_moyen).daily: un tableau, une ligne par jour :report_date,leads,applications,bookings,meetings,no_shows,sales,revenue,spend.breakdowns: un tableau, une ligne par valeur de la dimension :dimension_key,leads,applications,bookings,shows,no_shows,sales,revenue.
curl "https://app.durum.ai/api/v1/dashboard/breakdowns?from=2026-08-01&to=2026-08-31&dim=campaign" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"Publicités
| Route | Scope | Paramètres |
|---|---|---|
GET /ads/spend | read:ads | from, to |
GET /ads/campaigns | read:ads | from, to |
GET /ads/rules | read:ads | aucun |
GET /ads/rules/runs | read:ads | rule_id, limit (entier, défaut 30, max 100) |
Réponses :
spend: un tableau, une ligne par annonce et par plateforme :ad_id,ad_name,campaign_name,campaign_id,adset_name,adset_id,platform,total_spend,total_clicks,total_impressions,total_reach.campaigns: un tableau, une ligne par campagne et par plateforme, trié par dépense décroissante :campaign_id,campaign_name,platform,total_spend,total_clicks,total_impressions,total_reach,nb_ads,nb_adsets.rules: vos règles d'ads automatisées (libellé, type, campagne, état activé, mode simulation, paramètres, dernière exécution).rules/runs: les dernières exécutions d'une règle.
curl "https://app.durum.ai/api/v1/ads/rules/runs?rule_id=ID_DE_LA_REGLE&limit=10" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"Attribution
| Route | Scope | Paramètres |
|---|---|---|
GET /attribution | read:attribution | from, to |
GET /attribution/health | read:attribution | aucun |
GET /attribution/report | read:attribution | from, to |
Réponses :
attribution: un tableau, une ligne par annonce. Les colonnes sont préfixéesout_: identité (out_ad_id,out_ad_name,out_campaign_id,out_campaign_name,out_adset_id,out_adset_name,out_platform), média (out_total_spend,out_total_clicks,out_total_impressions,out_total_reach), résultats (out_leads,out_applications,out_bookings,out_meetings,out_meetings_present,out_meetings_no_show,out_sales,out_revenue,out_show_rate,out_close_rate,out_rep_names), répartition par modèle (out_sales_ftetout_revenue_ftpremier contact,out_sales_ltetout_revenue_ltdernier contact,out_sales_assistedetout_revenue_assisted), ajustements (out_refund_amount,out_dispute_amount,out_net_revenue) etout_is_excluded.health: un objet de santé de capture :verdict,events_pub_30j,pct_exact_id,pct_par_nom,pct_non_resolu,pixel_sessions_30j,pixel_derniere_activite,pixel_pct_avec_id,issues.report: un tableau par source, campagne et annonce :out_source,out_campaign,out_ad_id,out_ad_name,out_spend,out_gross_revenue,out_refund_amount,out_dispute_amount,out_net_revenue,out_roi_gross,out_roi_net,out_touchpoints.
curl "https://app.durum.ai/api/v1/attribution?from=2026-08-01&to=2026-08-31" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"Événements
| Route | Scope | Paramètres |
|---|---|---|
GET /events | read:events | type, from, to, limit (entier, défaut 50, max 200), cursor, rep |
GET /events/summary | read:events | from, to |
type accepte lead, application, booking, no_show, sale, cancel, payment, sale_lost. Sans type, tous les événements sont renvoyés. sale regroupe les ventes et les paiements ; application inclut les candidatures qualifiées ; booking inclut les RDV marketing et de suivi. rep filtre sur le nom du rep assigné.
events renvoie un tableau du plus récent au plus ancien, une ligne par événement : id, event_type, event_datetime, contact_email, contact_name, contact_phone, contact_id, value, amount, product_key, product_name, assigned_user_name, is_paid, payment_status, booking_date, booking_time, utm_source, utm_campaign, utm_content, campaign_name, calendar_name, meta_ad_id, is_baseline, cancelled_at, has_more, counted, drop_reason, group_key, contact_seq.
Pagination par curseur : has_more vaut true sur les lignes quand une page suivante existe. Pour l'obtenir, rappelez la route avec cursor égal au event_datetime de la dernière ligne reçue (horodatage ISO 8601) : la page suivante ne contient que les événements strictement antérieurs.
events/summary renvoie un tableau par annonce et campagne : ad_name, campaign_name, leads, applications, bookings, sales, no_shows, cancellations, total_value.
curl "https://app.durum.ai/api/v1/events?type=sale&from=2026-08-01&to=2026-08-31&limit=100" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"
# page suivante
curl "https://app.durum.ai/api/v1/events?type=sale&from=2026-08-01&to=2026-08-31&limit=100&cursor=2026-08-14T15:02:11Z" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"Ventes et finance
| Route | Scope | Paramètres |
|---|---|---|
GET /sales/summary | read:sales | from, to |
GET /sales/reps | read:sales | from, to |
GET /sales/cycle | read:sales | from, to |
GET /finance/pnl | read:finance | from, to |
Réponses :
sales/summary: un objet (KPI commerciaux, pipeline, no-show).sales/reps: un tableau, une ligne par rep :rep_name,bookings,meetings,shows,no_shows,sales,revenue,close_rate,no_show_rate,ticket_moyen,contacts_met,contacts_closed.sales/cycle: un objet :avg_days,median_days,p25_days,p75_days,sample_size,conversions,weekly_trend.finance/pnl: un objet de P&L. En plus de la liste globale des champs jamais renvoyés, cette route retire tout champ dont le nom contientmargin,profit,wholesale,markup,cost_ofoucogs.
curl "https://app.durum.ai/api/v1/sales/reps?from=2026-08-01&to=2026-08-31" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"Funnel
| Route | Scope | Paramètres |
|---|---|---|
GET /funnel | read:funnel | from, to |
Renvoie un objet : étapes du funnel, conversion par source, campagne et produit, délais de conversion.
curl "https://app.durum.ai/api/v1/funnel?from=2026-08-01&to=2026-08-31" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"Contacts
| Route | Scope | Paramètres |
|---|---|---|
GET /contacts | read:contacts | from, to, limit (entier, défaut 100, max 500), offset (entier, défaut 0), search |
GET /contacts/customers | read:contacts | aucun |
GET /contacts/purchases | read:contacts | from, to |
GET /contacts/calls | read:calls | email (obligatoire) |
Réponses :
contacts: un tableau, une ligne par contact :email,total_events,event_types,first_event_date,last_event_date,name,phone,sources,assigned_user_name,value,form_titles,has_campaign,tags,form_response. Pagination parlimitetoffset.contacts/customers: un tableau, une ligne par client :id,contact_email,contact_name,contact_phone,contact_id,first_event_type,first_event_date,latest_lead_date,first_payment_date,commission_status,commission_expires_at,total_revenue,total_purchases,total_commission,products,created_at.contacts/purchases: un tableau, une ligne par achat :id,purchase_date,product_key,product_name,contact_email,contact_name,amount,commissionable,commission_rate,commission_amount,invoice_id.contacts/calls: les appels des 365 derniers jours auxquels ce courriel a participé, avec l'analyse. Même objet quecallsci-dessous, pluscontact_email. Sansemail, la réponse est un tableau vide.
curl "https://app.durum.ai/api/v1/contacts?search=exemple.com&limit=200&offset=0" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"Appels
| Route | Scope | Paramètres |
|---|---|---|
GET /calls | read:calls | from, to, limit (entier, défaut 50, max 200), offset (entier, défaut 0) |
GET /calls/transcript | read:calls | id (entier, obligatoire) |
GET /calls/recording | read:calls | id (entier, obligatoire) |
calls est le point d'entrée : un tableau des appels analysés, du plus récent au plus ancien, une ligne par appel : artifact_id, starts_at, title, duration_sec, call_analysis_id, summary, call_type, is_qualified, icp_fit_score, qualification_reasoning, next_step_type, next_step_summary, next_step_proposed, scorecard_total, rep_id, markers (tableau de { ts, label, status }). L'artifact_id sert d'id aux deux routes suivantes.
calls/transcript renvoie la transcription brute : un tableau de segments dans l'ordre, chacun avec idx, start_ms, end_ms, speaker_id, speaker_label, speaker_email, text, confidence. Si l'appel n'appartient pas à votre compte, la réponse est 403 forbidden. Sans id, la réponse est un tableau vide.
calls/recording ne renvoie pas le fichier : il renvoie des URL signées, valables une heure, à télécharger ensuite directement :
{
"data": {
"artifact_id": 1234,
"content_type": "audio/mpeg",
"expires_in": 3600,
"audio_url": "https://…",
"video_url": null,
"has_video": false
}
}Erreurs propres à calls/recording : 400 invalid_id (id absent ou non positif), 403 forbidden (appel d'un autre compte ou inexistant), 404 no_recording_available (aucun fichier audio ni vidéo).
curl "https://app.durum.ai/api/v1/calls?from=2026-08-01&to=2026-08-31&limit=50" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"
curl "https://app.durum.ai/api/v1/calls/transcript?id=1234" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"
curl "https://app.durum.ai/api/v1/calls/recording?id=1234" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"Parcours et points de contact
| Route | Scope | Paramètres |
|---|---|---|
GET /journeys | read:journeys | from, to, limit (entier, défaut 50, max 200), offset (entier, défaut 0), search (fragment de courriel) |
GET /journeys/contact | read:journeys | email (obligatoire), pageviews (true ou 1 pour inclure les pages vues) |
GET /touchpoints | read:journeys | from, to, limit (entier, défaut 100, max 500), offset (entier, défaut 0), kind (web ou conversion) |
Ces routes exposent l'UTM capté à chaque point de contact, pas seulement le premier ou le dernier.
journeys: un objet{ coverage, paging, journeys }.coverageindique depuis quand les points de contact web existent (web_touchpoints_since) : avant cette date, l'absence de touch web ne signifie pas l'absence de visite.pagingcontientlimit,offset,returned. Chaque élément dejourneys:contact(email,phone,first_name,last_name),first_seen_at,last_seen_at,first_touchetlast_touch(occurred_at,event_type,is_paid,platform,utm,ad_ids),touchpoint_counts(web,conversion,total),milestones(first_lead_at,first_booking_at,first_sale_at,revenue),event_types_seen.fromettofiltrent sur la date de dernière activité du contact.journeys/contact: un objet{ contact, first_touch_at, last_touch_at, counts, touchpoints }.countscontienttotal,web,conversion,page_view.touchpointsest la chaîne chronologique complète : chaque point porte l'UTM (utm), les identifiants publicitaires et de clic captés à ce moment.touchpoints: un objet{ paging, touchpoints }, du plus récent au plus ancien, pour synchroniser vers votre entrepôt. Chaque point porte notammentutm,ad_ids,click_ids,channel,value,currency,product,is_imported. Unkindautre quewebouconversionrenvoie200avec{ "error": "invalid kind (web|conversion)", "touchpoints": [] }.
L'objet utm contient source, medium, campaign, content, term, campaign_name, adset_name, ad_name, channel, type, sequence, step.
curl "https://app.durum.ai/api/v1/journeys/contact?email=jean@exemple.com&pageviews=true" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"Créatifs
| Route | Scope | Paramètres |
|---|---|---|
GET /creatives/requests | read:creatives | status, unnotified (1 ou true), limit (entier, défaut 50, max 200) |
Les besoins créatifs sont les demandes que DURUM écrit quand il manque des créas. Sans status, la route renvoie les demandes en cours (open et partial) ; avec status, uniquement ce statut exact. unnotified=1 ne renvoie que les demandes jamais relayées à une équipe. Réponse : un tableau, de la plus ancienne à la plus récente, chaque élément avec id, reason, status, requested_count, filled_count, context, created_at, notified_at.
Le flux prévu pour un outil externe : lire avec unnotified=1, relayer à l'équipe, puis acquitter via POST /creatives/requests/ack (voir Écriture). L'acquittement pose seulement notified_at ; il ne ferme pas la demande.
curl "https://app.durum.ai/api/v1/creatives/requests?unnotified=1" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"ICP et pixels
| Route | Scope | Paramètres |
|---|---|---|
GET /icp/criteria | read:icp | aucun |
GET /capi/pixels | read:capi | aucun |
icp/criteria: l'objet JSON de vos critères de client idéal.capi/pixels: vos pixels Meta connectés et le pixel par défaut (configuration de routage). Les jetons d'accès ne sont jamais renvoyés : tout champ dont le nom contientaccess_token,secretouapi_keyest retiré avant envoi.
curl "https://app.durum.ai/api/v1/capi/pixels" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE"Écriture
Les écritures prennent un corps JSON. Un corps invalide renvoie 400 avec le message de validation dans error. Elles n'agissent que sur votre compte.
| Route | Méthodes | Scope | Corps |
|---|---|---|---|
/goals | PUT, POST | write:goals | { "goals": [ { "metric_key": "…", "target_value": 0 } ] } |
/sale-rule | PUT, POST | write:settings | { "sale_rule": { … } } |
/icp | PUT, POST | write:icp | { "icp": { … } } |
/ads/rules/enable | POST, PUT | write:ads | { "rule_id": "…", "enabled": true } |
/ads/rules/dry-run | POST, PUT | write:ads | { "rule_id": "…", "dry_run": true } |
/ads/rules/delete | POST | write:ads | { "rule_id": "…" } |
/creatives/upload-url | POST | write:creatives | { "content_type": "image/png", "label": "…" } |
/creatives/import | POST | write:creatives | { "external_ref": "…", "creative": { … } } |
/creatives/requests/ack | POST | write:creatives | { "ids": [ "uuid", … ] } |
Une méthode non listée pour la route répond 405 method_not_allowed.
Objectifs, règle de vente, ICP
/goals:goalsest un tableau d'objets{ metric_key, target_value }. Chaque objectif est enregistré sur la période mensuelle. Le corps sansgoalsrenvoie400 missing goals object. En cas de succès,datavaut{ "success": true }./sale-rule:sale_ruleest l'objet de votre règle de comptage des ventes (comment une vente est comptée et dédoublonnée). Corps sanssale_rule:400 missing sale_rule object./icp:icpremplace vos critères ICP. Corps sansicp:400 missing icp object.
curl -X PUT "https://app.durum.ai/api/v1/goals" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d '{"goals":[{"metric_key":"revenue","target_value":100000}]}'Règles d'ads
rule_id est obligatoire (sinon 400 missing rule_id). enabled et dry_run valent true sauf si vous envoyez explicitement false. Le mode simulation calcule sans agir sur Meta. La suppression est définitive. Une règle qui n'est pas la vôtre répond 403 forbidden. Armer une règle en réel (dry_run: false) demande un palier payant : sur le plan Découverte, le refus porte le code plan_free_no_autopilot (l'API elle-même y répond déjà 403 plan_free_no_api).
curl -X POST "https://app.durum.ai/api/v1/ads/rules/dry-run" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d '{"rule_id":"ID_DE_LA_REGLE","dry_run":true}'Dépôt d'une créa en deux étapes
Un dépôt atterrit en réserve dans votre librairie créative : il ne publie rien, ne touche à aucun budget et ne parle à aucune régie. Le visuel ne peut pointer que vers un fichier déposé via l'autorisation émise à l'étape 1 : une URL étrangère est refusée.
Étape 1 : POST /creatives/upload-url. Corps : content_type obligatoire, parmi image/jpeg, image/png, image/webp, video/mp4 (sinon 400) ; label optionnel (étiquette lisible, nettoyée, 60 caractères max). Le chemin de stockage est composé côté serveur dans le dossier de votre compte. Réponse :
{
"data": {
"asset_url": "https://…/creative-assets/…/mon-visuel-….png",
"upload_url": "https://…",
"token": "…",
"path": "…/2026/09/mon-visuel-….png",
"content_type": "image/png"
}
}Envoyez ensuite le fichier par PUT sur upload_url, avec le même Content-Type et le fichier binaire en corps. L'autorisation est à durée courte.
Étape 2 : POST /creatives/import. Corps : external_ref obligatoire (votre identifiant stable, 200 caractères max) et creative, un objet dont les champs reconnus sont angle, primary_text, headline, hook, cta, destination_url, asset_url (l'asset_url de l'étape 1, tel quel), asset_kind (image ou video), variant_label, visual_subtype, funnel, product, audience, video_script, source (objet libre de provenance). Réponse : { "id", "external_ref", "angle", "score", "en_attente" } où en_attente est le nombre de créas en réserve non publiées et score vaut null tant qu'il n'est pas calculé.
L'import est idempotent : rejouer le même external_ref met à jour la créa au lieu d'en créer une seconde. Une créa déjà publiée n'est jamais réécrite ; son id est simplement renvoyé.
curl -X POST "https://app.durum.ai/api/v1/creatives/upload-url" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d '{"content_type":"image/png","label":"offre-septembre"}'
curl -X PUT "UPLOAD_URL_RECUE" \
-H "Content-Type: image/png" \
--data-binary @offre-septembre.png
curl -X POST "https://app.durum.ai/api/v1/creatives/import" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d '{"external_ref":"drive-2026-09-offre-1","creative":{"angle":"Économies d’impôt","headline":"Réduisez vos impôts","primary_text":"…","asset_url":"ASSET_URL_RECUE","asset_kind":"image"}}'Acquittement des besoins créatifs
POST /creatives/requests/ack avec ids, un tableau non vide d'UUID de demandes (sinon 400 missing ids (array of request UUIDs)). Seules vos demandes jamais relayées sont marquées ; data est le nombre de demandes mises à jour. Rejouer le même lot est sans effet.
curl -X POST "https://app.durum.ai/api/v1/creatives/requests/ack" \
-H "Authorization: Bearer durum_sk_live_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d '{"ids":["3f2a…","9c1d…"]}'Champs jamais renvoyés
Avant envoi, chaque réponse est parcourue à tous les niveaux d'imbrication et tout champ dont le nom contient l'un des fragments suivants est retiré :
- identifiants et secrets :
access_token,refresh_token,api_key,apikey,secret,signing,service_role,password,webhook_secret,encryption,tokenenc,bearer; - internes d'IA :
model,model_name,provider,prompt,system_prompt,temperature,embedding; - coûts internes :
cost,cost_estimate,token_count,tokens_used,price_input,price_output; - marges :
gross_margin,net_profit,net_profit_after_ads,wholesale,markup,platform_fee,margin_pct,agency_margin,contribution_margin.
Toute valeur texte ressemblant à un identifiant de modèle d'IA est remplacée par DURUM.ai. finance/pnl ajoute ses propres fragments (voir Ventes et finance).
Codes de réponse
| Code | Corps | Signification |
|---|---|---|
200 | { "data": … } | succès |
400 | { "error": "…" } | paramètre ou corps invalide ; error contient le message (par exemple missing rule_id, invalid_id, external_ref is required) |
401 | { "error": "invalid_or_missing_api_key" } | clé absente, invalide, révoquée ou expirée |
403 | { "error": "insufficient_scope", "required": "…" } | la clé ne porte pas le scope exigé |
403 | { "error": "plan_free_no_api" } | l'espace de travail est sur le plan Découverte : l'API REST et le serveur MCP exigent un palier payant |
403 | { "error": "forbidden" } | la ressource n'appartient pas à votre compte |
404 | { "error": "unknown_route", "route": "/api/v1/…" } | route inconnue |
404 | { "error": "not_found" } | compte ou objet introuvable pour cette écriture |
404 | { "error": "no_recording_available" } | calls/recording sans fichier |
405 | { "error": "method_not_allowed" } | méthode non prise en charge pour cette route |
429 | { "error": "rate_limited", "retry_after_seconds": 60 } | limite de débit dépassée |
429 | { "error": "too_many_failed_attempts", "retry_after_seconds": 60 } | trop d'échecs d'authentification depuis votre adresse IP |
500 | { "error": "tenant_barrier" } | réponse bloquée par la vérification de compte ; contactez DURUM |
502 | { "error": "upstream_error" } | erreur interne lors de la lecture ou de l'écriture ; réessayez plus tard |