Guides · API REST
Recevoir et vérifier le callback
Une demande avec une callbackUrl se termine par un POST signé vers cette URL : les réponses quand le destinataire soumet, ou l'annonce qu'elle a expiré ou a été annulée. Ce guide exécute un récepteur sur votre machine, vérifie la signature, et affiche les réponses.
Last checked
Il vous faut un token API et un formulaire publié, comme dans Envoyer une demande avec l’API REST, et Node ou Python sur votre machine. Ce que porte un callback, et comment fonctionnent les nouvelles tentatives, est dans Callbacks et signature.
1. Copiez le secret de signature
formbase signe chaque callback avec le secret de signature des demandes de votre espace de travail. Ouvrez OAuth et clés API dans la barre latérale de l’espace de travail et trouvez la carte Secret de signature des demandes :

Cliquez sur le bouton de copie et gardez le secret dans une variable d’environnement dans le terminal où votre récepteur s’exécutera :
export FORMBASE_SIGNING_SECRET='rqs_...'Ne le régénérez pas
Le troisième bouton crée un nouveau secret, et l’ancien cesse de fonctionner immédiatement, aussi pour les callbacks déjà en chemin. Vous n’en avez pas besoin pour suivre ce guide.
2. Écrivez le récepteur
Enregistrez la fonction verify depuis Callbacks et signature à côté de votre
récepteur : sous verify.mjs pour Node, puisqu’il utilise import, ou sous verify.py pour Python. Le
récepteur lit le corps brut, vérifie la signature, et ne parse le JSON qu’ensuite :
import http from 'node:http'
import { verifyFormbaseCallback } from './verify.mjs'
const secret = process.env.FORMBASE_SIGNING_SECRET
http
.createServer((req, res) => {
const chunks = []
req.on('data', (chunk) => chunks.push(chunk))
req.on('end', () => {
const rawBody = Buffer.concat(chunks).toString('utf8')
const signature = req.headers['x-formbase-signature'] ?? ''
if (!verifyFormbaseCallback(rawBody, signature, secret)) {
console.log('rejected: bad signature')
res.writeHead(401).end()
return
}
const event = JSON.parse(rawBody)
console.log(event.type, event.data.request.externalId, JSON.stringify(event.data.answers))
res.writeHead(200).end()
})
})
.listen(8787)import json, os
from http.server import BaseHTTPRequestHandler, HTTPServer
from verify import verify_formbase_callback
SECRET = os.environ['FORMBASE_SIGNING_SECRET']
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
raw_body = self.rfile.read(int(self.headers['Content-Length']))
signature = self.headers.get('X-formbase-Signature', '')
if not verify_formbase_callback(raw_body, signature, SECRET):
print('rejected: bad signature', flush=True)
self.send_response(401)
self.end_headers()
return
event = json.loads(raw_body)
print(event['type'], event['data']['request'].get('externalId'), event['data'].get('answers'), flush=True)
self.send_response(200)
self.end_headers()
HTTPServer(('', 8787), Handler).serve_forever()Hachez le corps exactement comme il est arrivé. Le parser puis le retransformer en JSON change les octets, et la signature ne correspond plus.
3. Exécutez-le et donnez-lui une URL publique
Démarrez le récepteur :
node receiver.mjs # or: python3 receiver.pyformbase n’appelle que des adresses HTTPS publiques, donc localhost ne suffira pas. Pendant que vous testez, un tunnel en
donne une à votre machine.
Le tunnel rapide de Cloudflare
ne nécessite aucun compte. Exécutez-le dans un second terminal :
cloudflared tunnel --url http://localhost:8787Il affiche une adresse comme https://horn-cod-classics-arab.trycloudflare.com. ngrok et les outils similaires fonctionnent de
la même façon. En production, utilisez plutôt l’URL HTTPS propre à votre serveur.
4. Envoyez une demande avec une URL de callback
Créez une demande de test comme dans le guide précédent, et ajoutez
callbackUrl avec l’adresse du tunnel. Utilisez votre propre ID de formulaire :
curl -X POST https://api.formbase.so/api/v1 \
-H "Authorization: Bearer $FORMBASE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"method": "requests.create",
"params": {
"formId": "jx75hdx8vb5hy1x85gm17nqgkn8f674g",
"recipient": { "email": "ada@acme.example", "name": "Ada Lovelace" },
"prefill": { "company_name": "Analytical Engines Ltd" },
"readonly": ["company_name"],
"externalId": "supplier-2044",
"idempotencyKey": "supplier-2044",
"callbackUrl": "https://horn-cod-classics-arab.trycloudflare.com/formbase",
"test": true
}
}'Utilisez une nouvelle idempotencyKey : la même clé avec un corps différent est rejetée. Ouvrez url depuis la
réponse, répondez au formulaire, et soumettez.
5. Voyez le callback arriver
Quelques secondes après votre soumission, le récepteur affiche le type d’événement, votre ID externe et les réponses. Le récepteur Node affiche :
request.completed supplier-2044 {"certificate_of_incorporation":[{"name":"certificate-of-incorporation.pdf","size":635,"type":"application/pdf","url":"https://api.formbase.so/api/storage/...",...}],"company_name":"Analytical Engines Ltd","contact_email":"ada@acme.example","do_you_accept_our_30_day_payment_terms":"yes","vat_number":"GB123456789"}Pour voir un rejet, envoyez vous-même n’importe quel POST au récepteur, par exemple
curl -X POST -d ‘’ http://localhost:8787. Il n’a pas de signature valide, donc le récepteur affiche
rejected: bad signature et répond 401.
Avant de passer en production
Répondez 2xx en moins de 10 secondes. formbase marque le callback comme livré sur n’importe quel 2xx. Un 5xx, un 408, un 429 ou un délai dépassé est retenté pendant environ quatre heures ; tout autre 4xx, comme le 401 ci-dessus, arrête les tentatives. Voir Nouvelles tentatives.
Bifurquez sur
type.La même URL entend aussi
request.expiredetrequest.canceled, qui ne portent aucune réponse.Traitez chaque événement une fois. Une nouvelle tentative porte le même
id. Stockez les ids déjà traités et ignorez les répétitions.Vérifiez
test.Un callback venant d’une demande de test porte
“test”: true. Retireztestderequests.createpour les destinataires réels.
Si votre récepteur était hors service plus longtemps que la durée des tentatives, les réponses ne sont pas perdues. Relisez-les avec
requests.get, ou renvoyez le callback avec
requests.replayCallback ou depuis la
page Demandes.