Presuasion submissions
1 endpoint
/v1/presuasion-submissionsSoumettre un formulaire d'outil pré-suasion
Endpoint public — convention legacy pré-CAP-440. Cet endpoint
s'écarte des conventions standard de l'API v1 sur plusieurs points
documentés ci-dessous. Il est appelé par la gateway pré-suasion
(demo.capturia.io) quand un visiteur complète un formulaire
d'outil pré-suasion publié sur le custom domain d'un client
Capturia.
Authentification : défense en profondeur
Pas de clé API Bearer (l'endpoint est appelé par une gateway publique, pas par un client tiers). Trois couches superposées protègent l'endpoint :
- Rate-limit IP (
RATE_LIMIT_CONFIG.LEAD_CREATION_LIMITreq parLEAD_WINDOW_SECONDS) — coupe le bruit haute fréquence. - Signature HMAC
X-Capturia-Signature: t=<unix>,v1=<hex>— pattern Stripe webhook, SHA-256 sur${timestamp}.${rawBody}, fenêtre replay 5 min, rotation 2-clés (PRESUASION_HMAC_SECRET+PRESUASION_HMAC_SECRET_PREVIOUS). Mode log-only par défaut, flip fail-closed viaPRESUASION_HMAC_REQUIRED=true. - Token Cloudflare Turnstile (champ
turnstile_tokendu body) vérifié siteverify avecidempotency_key=submission_iddu body (stable entre les retries d'une même soumission — un token est à usage unique, Cloudflare rejoue le verdict original sur la même clé), avec repli sur lerequest_idde la requête quand la soumission ne porte pas desubmission_id. Mode log-only par défaut, flip fail-closed viaPRESUASION_TURNSTILE_REQUIRED=true.
En complément, un quota par proposition_slug (100 submissions
par jour UTC, Upstash KV) borne le blast radius d'une attaque ciblée
même si les autres couches sont contournées.
Le client cible est résolu serveur-side via proposition_slug →
presuasion_instances.client_id — le client_id n'est jamais
accepté depuis le body.
Body et réponse au format custom
Le body suit un schéma propre à cet endpoint
(PresuasionSubmissionPayload). La réponse 200 retourne un objet
plat {success, contact_id, contact_created, activity_id} au lieu
de l'enveloppe v1 standard {data: ...}. Les erreurs retournent
{error: "string", issues?: [...]} au lieu d'ErrorEnvelope.
Comportement
- Resolve le client cible via
proposition_slugactif. - Upsert le contact par
(lower(email), client_id)— enrichit les champs absents (first_name,phone) sans jamais écraser les valeurs existantes non-null. - Insère une activité
presuasion_submissiondans la timeline du contact avec un bloc humainQ/Rformaté +metadatabrute (responses, result_data, tool_id/name, proposition).
Rate limit
Émet les headers legacy X-RateLimit-* (compteur IP) — pas les
headers RateLimit-* RFC 9331 du reste de l'API v1.
Headers
| Name | Type | required | Description |
|---|---|---|---|
X-Capturia-Signature | string | optional | Signature HMAC SHA-256 calculée par le gateway sur `${timestamp}.${rawBody}` avec `PRESUASION_HMAC_SECRET`. Fenêtre de replay 5 minutes. Optionnel en mode log-only ; obligatoire quand `PRESUASION_HMAC_REQUIRED=true` (réponse 401 sinon). |
Request body
#/components/schemas/PresuasionSubmissionPayload
Responses
| Status | Description |
|---|---|
200 | Soumission enregistrée. Contact upserté + activité
|
400 | Body JSON invalide ou payload Zod invalide. Convention
legacy — body custom |
401 | Signature HMAC |
403 | Token Cloudflare Turnstile manquant, expiré ou rejeté par
siteverify. Émis uniquement quand
|
404 |
|
409 | Le |
429 | Deux variantes possibles :
|
500 | Erreur serveur (lookup proposition, upsert contact ou insert
activité). Convention legacy — body custom
|
Example request
curl -X POST 'https://app.capturia.io/api/v1/presuasion-submissions' \
-H 'Authorization: Bearer cap_live_<your-key>' \
-H 'Content-Type: application/json' \
-d '{
"proposition_slug": "blueprint-ventes-2026",
"tool_id": "calculateur-roi",
"tool_name": "Calculateur de ROI",
"email": "[email protected]",
"first_name": "Alex",
"phone": "+15145551234",
"responses": {
"budget": "500-1000",
"timeline": "3-mois"
},
"formatted_answers": [
{
"question": "Quel est ton défi principal en ventes ?",
"answer": "Je perds des leads parce que je ne suis pas assez rapide à les rappeler."
}
],
"result_data": {
"score": 87,
"recommendation": "premium"
},
"completed_at": "2026-05-04T12:34:56.789Z"
}'