Leads
1 endpoint
/v1/leads/captureCapturer un lead depuis un funnel externe
Capture un nouveau lead dans Capturia depuis un funnel, formulaire ou outil tiers (Make, Zapier, n8n, landing custom). Une fois capturé, le lead apparaît immédiatement dans Mes Prospects côté dashboard et le bot SMS prend le relais dans les 30 secondes si le consentement et la configuration sont en ordre.
Contact déjà connu (courriel ou téléphone)
La fiche est résolue par courriel d'abord (insensible à la casse),
puis par téléphone, en incluant les coordonnées secondaires déjà
rattachées à une fiche. Capturer un lead dont le courriel ou le
téléphone existe déjà réutilise et enrichit la fiche existante — le
contact_id retourné est celui de cette fiche, jamais un doublon et
jamais une erreur. Les coordonnées et champs cœur déjà remplis (nom,
courriel, téléphone, pipeline, valeur, adresse...) ne sont pas
écrasés ; les réponses de qualification (custom_fields) sont en
revanche rafraîchies avec les dernières valeurs envoyées — une clé
déjà présente est remplacée, une clé nouvelle s'ajoute. Un nouveau
téléphone est conservé comme numéro secondaire quand la fiche en a
déjà un. Si le courriel et le téléphone pointent deux fiches
différentes, le courriel a priorité et aucune coordonnée n'est retirée
à l'autre fiche.
La réponse expose contact_was_new pour distinguer les deux issues :
sur une fiche existante (false), pipeline et assignment_status
décrivent la demande résolue, pas forcément l'état persisté — la fiche
conserve son pipeline, son étape et son vendeur déjà en place, et
ignored_fields nomme ceux des champs du bloc pipeline qu'elle n'a
pas pris (existing_contact_kept).
Champs reçus non pris (ignored_fields)
Une capture qui répond 200 dit ce qu'elle n'a pas pris. La réponse
porte ignored_fields, une liste [{ field, reason }] toujours
présente et vide quand tout a été appliqué :
unknown_field— un nom que l'API ne connaît pas, à la racine, danscustomDataou dans un bloc (pipeline.id,utm.utm_source) ;custom_field_not_defined— une clé decustom_fieldssans définition du même nom dans le compte ;reserved_tag— une étiquette au nom réservé (tags[1]) ;system_not_active— unsystem_idqui n'est pas actif ;existing_contact_kept— un champ du blocpipelineque la fiche existante a conservé ;sales_rep_not_found— un courriel de vendeur qui ne désigne aucun membre actif du compte ;submission_not_recorded— ce qui ne s'applique qu'avec une soumission enregistrée, quand elle ne l'est pas (existing_pipeline_contact,idempotent_replay) ;duplicate_field— une copie d'un champ reçu par plusieurs sources, écartée au profit d'une autre valeur.
Seule limite : sur une fiche existante, les champs d'identité et
d'adresse déjà remplis, et l'attribution publicitaire déjà posée
(règle du premier contact), sont conservés sans être nommés un à un
(contact_was_new: false le signale).
Le lead est capturé dans tous ces cas : c'est un avertissement de
montage, à lire au premier test d'une intégration. La liste est
plafonnée à 50 entrées (ignored_fields_truncated), les champs mappés
à la main avant ceux qu'un connecteur ajoute d'office.
Pipeline et étape
Le bloc pipeline désigne le pipeline et l'étape par leur identifiant
texte (pipeline_slug, stage_slug), à copier dans le dashboard :
Administration, Clés API, section « Identifiants de tes pipelines et
étapes ». Un connecteur qui n'envoie que des paires clé/valeur
(workflow GoHighLevel, Zapier) les envoie à plat :
pipeline.pipeline_slug, pipeline.stage_slug. Sans
pipeline_slug, l'étape est cherchée dans le pipeline par défaut.
Contact déjà en pipeline ouvert
Un lead dont le contact (résolu par courriel ou téléphone) est déjà dans
une étape de pipeline ouverte (déjà pris en charge par un vendeur — non
archivé, étape ni gagnée ni perdue)
n'est pas re-soumis : la réponse renvoie status: existing_pipeline_contact,
aucune nouvelle soumission n'est enregistrée et le bot SMS n'est pas
redéclenché, pour ne pas écraser une conversation en cours. Les champs
cœur nouvellement fournis enrichissent quand même le contact existant.
La dédup de retry reste prioritaire : un re-push reçu dans la fenêtre de
5 minutes est traité comme un retry (status: idempotent_replay, voir
Idempotency) et conserve l'activity_id et l'original_created_at
d'origine — existing_pipeline_contact ne s'applique qu'au-delà de cette
fenêtre.
Idempotency
Le header Idempotency-Key est optionnel mais recommandé pour les retries
côté client. Une 2e requête avec la même clé et le même payload renvoie la
réponse d'origine sans dupliquer le lead. La clé est stockée 24h. Une 2e
requête avec la même clé mais un payload différent retourne 409
idempotency_conflict. La RPC sous-jacente conserve par ailleurs sa propre
fenêtre de dedup à 5 minutes. Cette fenêtre est par contact et
indépendante du contenu : toute soumission d'un contact déjà capturé
dans les 5 dernières minutes est traitée comme un retry
(idempotent_replay) même si le payload diffère — la fiche est tout de
même enrichie (dont les custom_fields, rafraîchis), mais aucune
nouvelle activité, aucun tag, et pas de relance du bot SMS. Les mises à
jour de la fiche restent réelles : une automatisation qui surveille un
champ personnalisé réagit au changement de valeur, comme pour toute
modification de la fiche. C'est aussi ce qui protège d'une double
relance du bot quand un lead re-soumet son formulaire coup sur coup.
Un Idempotency-Key différent ne contourne pas cette fenêtre : elle
s'applique en base après tout cache miss. Deux soumissions
volontairement distinctes du même contact ne produisent deux activités
que si elles sont espacées de plus de 5 minutes.
Codes d'erreur custom
Pour des raisons historiques, certains rejets retournent des codes hors catalog standard :
- 422 —
invalid_phone,missing_consent,invalid_email,invalid_payload,pipeline_not_found,stage_not_found(au lieu devalidation_error/business_rule_violation). Body shape simplifiée propre à cette route — voir la réponse 422 ci-dessous. - 429 —
rate_limited_ipquand la limite IP (100 req/min) est atteinte (au lieu derate_limit_exceeded). Le headerX-RateLimit-Scope: ipidentifie le scope de la limite déclenchée. Body shape simplifiée (pas derequest_idnidetailsstandard) — à harmoniser dans une phase ultérieure.
leads:captureCorps de requête
| Propriété | Type | Obligatoire | Description |
|---|---|---|---|
phone | string | integer | obligatoire | Téléphone (sera normalisé E.164). Format final attendu :
^\+[1-9]\d{10,14}$ (entre 11 et 15 chiffres après le +).
Aussi accepté : un entier JSON positif dans la zone sûre JavaScript
(au plus 9007199254740991 — tout numéro E.164 y tient), converti en
chaîne avant normalisation ; connecteurs no-code dont le champ source
est numérique. Préférer la chaîne — seule forme qui préserve un +
international explicite. Tout autre nombre (négatif, décimal, zéro)
est refusé en 422 invalid_phone avec le rappel des formats acceptés. |
sms_consent | boolean | string | obligatoire | Consentement SMS explicite. Doit être true (boolean) ou une
chaîne truthy whitelisted (voir description du schema). Les
valeurs ambiguës (false, "no", 0, etc.) sont rejetées avec
422 missing_consent. Accepté à la racine du body ou dans
l'objet customData (webhooks GoHighLevel). |
first_name | string | optionnel | — |
last_name | string | optionnel | — |
email | string | optionnel | — |
source_label | string | optionnel | Libellé libre identifiant la source du lead (ex funnel-fb-jan,
landing-blueprint). Affiché dans le pipeline et le journal
d'activité du contact. |
utm | LeadCaptureUtm | optionnel | — |
gclid | string | optionnel | Identifiant de clic Google Ads (paramètre gclid de l'URL
d'atterrissage, auto-tagging). Conservé avec l'attribution
publicitaire du lead (first-touch). Sans utm.source, le lead est
attribué à la source google et au support cpc, ce qui le
rattache à la campagne dont la provenance web est google / cpc.
Quand meta.ads_consent vaut false, l'identifiant n'est jamais
conservé (ni dans l'attribution ni dans la copie reçue) : seule la
provenance google / cpc reste. |
notes | string | optionnel | Notes libres associées au lead (visible dans la fiche contact). |
tags | string[] | optionnel | Tags à associer au contact créé. Si un tag n'existe pas pour ce
client, il est créé à la volée. Tout nom qui commence par
« Campagne : » (casse et espaces libres) est ignoré : ce début de nom
est réservé aux tags de campagne, que Capturia pose seul d'après la
campagne du lead. La réponse le nomme par sa position dans
ignored_fields (tags[1], raison reserved_tag). |
qualification_answers | array | optionnel | Liste de questions/réponses de qualification capturées en amont. |
pipeline | LeadCapturePipelineConfig | optionnel | — |
custom_fields | object | optionnel | Champs personnalisés du contact, keyés par le NAME de la définition
configurée côté client (ex type_propriete), pas par son UUID interne —
l'integrator d'un site ne connaît pas les ids. La capture résout
name → définition par client puis remplit la fiche contact et le kanban.
Une clé sans définition correspondante est ignorée (pas d'erreur) et
nommée dans ignored_fields de la réponse
(custom_fields.ma_cle, raison custom_field_not_defined). Pour un
champ select, fournir l'option value (ex maison_isolee), pas le
label. |
address | string | optionnel | Adresse postale du contact (texte libre, ex la propriété à vendre). |
postal_code | string | optionnel | Code postal du contact. |
address_place_id | string | optionnel | Identifiant de lieu (ex Google Place ID) si l'adresse provient d'un autocomplete. Stocké tel quel pour ré-résolution ultérieure. |
street | string | optionnel | Rue (numéro civique compris). Complète une fiche existante sans écraser une rue déjà remplie. |
city | string | optionnel | Ville. Complète une fiche existante sans écraser une ville déjà remplie. |
sector | string | optionnel | Secteur ou quartier. Complète une fiche existante sans écraser un secteur déjà rempli. |
province | string | optionnel | Province. Complète une fiche existante sans écraser une province déjà remplie. |
country | string | optionnel | Pays. Complète une fiche existante sans écraser un pays déjà rempli. |
meta | LeadCaptureMetaAttribution | optionnel | — |
system_id | string | optionnel | Système cible de la capture (UUID d'un Système du compte, visible dans
le dashboard Capturia). Le lead capturé est engagé dans ce Système.
Absent → Système principal (comportement historique). Un UUID qui ne
correspond à aucun Système du compte → erreur system_not_found (422),
aucun contact créé. Un Système du compte qui n'est pas actif (archivé,
par exemple) → le lead va au Système principal, et la réponse nomme
system_id dans ignored_fields (raison system_not_active). |
Responses
| Statut | Description |
|---|---|
200 | Lead capturé ( |
400 | La requête est mal formée (header manquant, JSON invalide, paramètre invalide). |
401 | Clé API inconnue ou format invalide. |
403 | Clé sans le scope |
409 | Une requête précédente avec la même |
422 | Payload refusé par la validation ( |
429 | Le quota de requêtes par fenêtre est dépassé pour cette clé API ou cet endpoint. |
500 | Erreur serveur inattendue. Toujours fournir le |
Exemple de requête
curl -X POST 'https://api.capturia.io/v1/leads/capture' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json' \
-d '{
"phone": "+15145551234",
"sms_consent": true,
"first_name": "Alex",
"last_name": "Tremblay",
"email": "[email protected]",
"source_label": "funnel-fb-jan",
"utm": {
"source": "facebook",
"medium": "cpc",
"campaign": "spring-2026"
},
"tags": [
"lead-chaud",
"vu-webinaire"
],
"pipeline": {
"pipeline_slug": "ventes-b2b",
"stage_slug": "nouveau-lead",
"deal_value": 2500,
"expected_close_date": "2026-06-30",
"assigned_sales_rep_email": "[email protected]"
},
"custom_fields": {
"type_propriete": "maison_isolee",
"delai_vente": "0_3_mois"
},
"address": "123 rue Principale, Montréal",
"postal_code": "H2X 1Y4"
}'