formbasedocs
Aller à l'applicationAppli

Demandes

Callbacks et signature

Quand une demande atteint sa fin — terminée, expirée, ou annulée — formbase envoie en POST une notification signée vers l'URL fournie par votre automatisation. Cet appel est ce qui reprend l'exécution.


Ce qui se déclenche, et quand

ÉvénementQuand
request.completedLe destinataire a soumis. Porte les réponses.
request.expiredL'expiration est passée pendant que la demande était encore en attente.
request.canceledVous ou votre automatisation l'avez retirée.

Les trois arrivent à la même URL, alors testez type avant de supposer qu’il y a des réponses. C’est tout l’intérêt de se déclencher sur chaque fin : un workflow mis en pause sur un client reprend qu’il ait répondu, vous ait ignoré, ou ait été annulé.

Ce qui arrive

Corps du POST
json
{
  "id": "evt_kj7...",
  "type": "request.completed",
  "createdAt": "2026-03-04T09:31:40.000Z",
  "apiVersion": "2026-09-24",
  "test": false,
  "data": {
    "request": {
      "id": "kd7...",
      "externalId": "run-42",
      "status": "completed",
      "outcome": "approve",
      "language": "en",
      "recipient": { "email": "ada@acme.com", "name": "Ada" },
      "metadata": { "runId": "run-42" },
      "context": { "case_id": "CASE-9" },
      "createdAt": "2026-03-04T09:20:00.000Z",
      "completedAt": "2026-03-04T09:31:40.000Z"
    },
    "form": { "id": "j57...", "name": "Vendor onboarding", "snapshotId": "kx2..." },
    "submission": {
      "id": "jd7...",
      "respondentEmail": "ada@acme.com",
      "submittedAt": "2026-03-04T09:31:40.000Z",
      "updatedAt": null,
      "editCount": 0,
      "pdfUrl": null,
      "language": "en"
    },
    "answers": { "company_name": "Acme", "contacts": [{ "name": "Ada" }] },
    "display": { "company_name": "Acme", "contacts": "Ada" }
  }
}
  • data.request est toujours là — y compris votre externalId, vos metadata, et votre context, inchangés. Elle porte l’horodatage de la fin qui s’est produite (completedAt, expiredAt, ou canceledAt avec un cancelReason optionnel).

  • outcome n’est présent que lorsque le destinataire a répondu à une question de décision — approve, decline, ou changes.

  • form, submission, answers et display n’apparaissent qu’à la fin réussie, exactement dans la forme que porte un webhook de soumission. form.snapshotId est la version publiée exacte à laquelle le destinataire a répondu ; submission.pdfUrl est une URL uniquement quand le formulaire conserve un PDF de soumission, et null sinon.

  • test vaut true quand la demande a été créée en mode test — bifurquez dessus, ou ignorez l’événement.

  • answers est indexé par clé de champ, les groupes répétables étant imbriqués comme un objet par instance. Une réponse à choix est la clé de l’option depuis fields.list, pas son libellé ; le libellé se trouve dans display, sous la même clé.

  • Le POST arrive en Content-Type: application/json avec User-Agent: formbase, et porte X-formbase-Event-Id, X-formbase-Event-Type et X-formbase-Signature — pour dédupliquer et router avant même d’analyser le corps.

Vérifier la signature

Chaque callback porte un en-tête de signature, X-formbase-Signature: t={secondes unix},sha256={hex}. La valeur hex est un HMAC-SHA256 de l’horodatage, d’un point, et du corps brut de la requête, calculé avec le secret de signature de demande de votre espace de travail.

Deux règles, quel que soit le langage utilisé :

  1. Hachez le corps brut, avant tout analyse ou re-sérialisation. Un JSON réencodé n’est pas les mêmes octets.

  2. Comparez en temps constant — crypto.timingSafeEqual, hmac.compare_digest — jamais avec ==.

Le secret de signature de demande

Un seul secret par espace de travail signe chaque callback qui en provient. Trouvez-le dans OAuth et clés API dans la barre latérale de l’espace de travail, dans la carte Secret de signature de demande. Il est masqué par défaut ; Révéler le secret l’affiche et le bouton copier le copie. Ce n’est pas une valeur à usage unique — vous pouvez revenir le consulter plus tard. Le secret est créé la première fois qu’il est nécessaire, donc un espace de travail qui n’a jamais ouvert cette carte et jamais créé de demande avec un callbackUrl n’en a encore aucun.

Nouvelles tentatives

Un callback obtient huit tentatives : la première, puis sept nouvelles tentatives espacées d’au moins 1, 2, 4, 8, 16, 32 et 60 minutes. formbase recherche les tentatives dues toutes les 30 minutes, une nouvelle tentative peut donc arriver jusqu’à une demi-heure après la fin de son intervalle, et la dernière tentative arrive environ quatre heures après la fin de la demande. Chaque tentative porte les mêmes octets et le même id : la charge utile est figée au moment où la demande s’est terminée, donc une nouvelle tentative décrit ce qui s’est passé alors, pas ce à quoi ressemble la demande maintenant. L’URL de destination et le secret de signature sont lus à chaque tentative, non figés avec elle.

Votre réponseCe que fait formbase
2xxTerminé. Le callback est marqué comme livré.
408, 429, 5xxNouvelle tentative, en respectant Retry-After si vous en envoyez un.
Autre 4xxArrêt. Votre point de terminaison a rejeté l'appel, retenter les mêmes octets ne peut rien changer.
Délai dépassé ou erreur de connexionNouvelle tentative selon le même planning.
URL bloquéeArrêt immédiat. Un hôte qui ne se résout pas, une adresse privée, ou une URL non-HTTPS ne peut jamais devenir autorisé. Les redirections ne sont jamais suivies, donc un 3xx s'arrête aussi.

Si le budget est épuisé — votre récepteur était indisponible tout l’après-midi — le callback n’est pas perdu. La demande obtient un badge Callback échoué, le propriétaire de l’espace de travail reçoit un e-mail unique avec l’hôte, le motif et le nombre de tentatives, et les réponses restent lisibles via requests.get. Pour la renvoyer, ouvrez la demande dans la page Demandes et appuyez sur Rejouer, ou appelez requests.replayCallback. Cela renvoie la même charge utile figée avec le même id, ce qui est exactement ce que veut un récepteur qui déduplique.

Les abonnements entendent les mêmes événements

Une URL de callback appartient à une seule demande. Quand chaque demande d’un formulaire doit atteindre le même récepteur, abonnez-vous une seule fois à la place : les applications formbase pour Zapier et n8n le font pour vous, et webhooks.create le fait depuis du code avec request_completed, request_expired ou request_canceled comme type d’événement. Un abonnement reçoit cette même enveloppe, signée avec son propre secret plutôt qu’avec le secret de signature de demande de l’espace de travail, avec son propre id d’événement et son propre budget de nouvelles tentatives. Une demande qui a à la fois une URL de callback et un abonnement correspondant se déclenche deux fois, une fois vers chacun. Rejouer ne renvoie que le callback ; un abonnement retente de lui-même et se met en pause après cinq tentatives échouées.

Une URL de reprise n'est pas une authentification

Les outils de workflow vous remettent une URL de reprise difficile à deviner, et il est tentant de considérer ça comme une preuve. C’est un secret porteur — il peut fuiter dans des journaux, et il ne vous dit pas que le corps n’a pas été altéré. Vérifiez la signature dans la branche de reprise aussi.