EN

Presuasion submissions

1 endpoint

post/v1/presuasion-submissions

Soumettre 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 :

  1. Rate-limit IP (RATE_LIMIT_CONFIG.LEAD_CREATION_LIMIT req par LEAD_WINDOW_SECONDS) — coupe le bruit haute fréquence.
  2. 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 via PRESUASION_HMAC_REQUIRED=true.
  3. Token Cloudflare Turnstile (champ turnstile_token du body) vérifié siteverify avec idempotency_key = submission_id du 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 le request_id de la requête quand la soumission ne porte pas de submission_id. Mode log-only par défaut, flip fail-closed via PRESUASION_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_slugpresuasion_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_slug actif.
  • 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_submission dans la timeline du contact avec un bloc humain Q/R formaté + metadata brute (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

NomTypeobligatoireDescription
X-Capturia-SignaturestringoptionnelSignature 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).

Corps de requête

#/components/schemas/PresuasionSubmissionPayload

Responses

StatusDescription
200

Soumission enregistrée. Contact upserté + activité presuasion_submission créée dans la timeline.

400

Body JSON invalide ou payload Zod invalide. Convention legacy — body custom {error, issues?} non aligné avec ErrorEnvelope du catalog v1.

401

Signature HMAC X-Capturia-Signature manquante, mal formée, expirée (fenêtre 5 min) ou invalide. Émis uniquement quand PRESUASION_HMAC_REQUIRED=true (mode fail-closed). En mode log-only, les requêtes non signées passent et un warning est émis pour observabilité.

403

Token Cloudflare Turnstile manquant, expiré ou rejeté par siteverify. Émis uniquement quand PRESUASION_TURNSTILE_REQUIRED=true (mode fail-closed).

404

proposition_slug inconnu ou désactivé (presuasion_instances.is_active = false).

409

Le proposition_slug existe mais n'est pas relié à un client Capturia (client_id IS NULL). Erreur de configuration côté admin — la soumission est rejetée plutôt que silencieusement droppée.

429

Deux variantes possibles :

  • Rate limit IP dépassé — body {error: "Too many requests"}, headers X-RateLimit-* (convention legacy).
  • Quota par proposition_slug dépassé (100/jour UTC) — body {error: "Slug quota exceeded"} accompagné du header standard Retry-After (secondes jusqu'à minuit UTC) en plus des X-RateLimit-*.
500

Erreur serveur (lookup proposition, upsert contact ou insert activité). Convention legacy — body custom {error: "string"} au lieu d'ErrorEnvelope.

Exemple de requête

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"
}'