formbasedocs
Naar de appApp

Aanvragen

Callbacks & ondertekening

Wanneer een aanvraag zijn einde bereikt — voltooid, verlopen, of geannuleerd — post formbase een ondertekende melding naar de URL die je automatisering heeft opgegeven. Die aanroep is wat de run hervat.


Wat vuurt af, en wanneer

GebeurtenisWanneer
request.completedDe ontvanger heeft ingediend. Bevat de antwoorden.
request.expiredDe vervaltermijn is verstreken terwijl de aanvraag nog in behandeling was.
request.canceledJij of je automatisering heeft hem ingetrokken.

Alle drie komen aan op dezelfde URL, dus vertak op type voordat je aanneemt dat er antwoorden zijn. Dat is het hele punt van afvuren bij elk einde: een workflow die geparkeerd stond op een klant hervat, of ze nu geantwoord hebben, je genegeerd hebben, of afgeblazen zijn.

Wat er binnenkomt

POST body
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 is er altijd — inclusief jouw externalId, metadata, en context, ongewijzigd. Hij draagt de tijdstempel van welk einde er ook gebeurde (completedAt, expiredAt, of canceledAt met een optionele cancelReason).

  • outcome is alleen aanwezig wanneer de ontvanger een beslissingsvraag heeft beantwoord — approve, decline, of changes.

  • form, submission, answers en display verschijnen alleen bij voltooiing, in precies de vorm die een inzendingswebhook draagt. form.snapshotId is de exacte gepubliceerde versie waarop de ontvanger antwoordde; submission.pdfUrl is alleen een URL wanneer het formulier een inzendings-PDF bewaart, en anders null.

  • test is true wanneer de aanvraag is aangemaakt in testmodus — vertak erop, of laat het event vallen.

  • answers is gesorteerd op veldsleutel, met herhaalgroepen genest als één object per instantie. Een keuzeantwoord is de sleutel van de optie uit fields.list, niet het label; het label staat in display, onder dezelfde sleutel.

  • De POST komt binnen als Content-Type: application/json met User-Agent: formbase, en draagt X-formbase-Event-Id, X-formbase-Event-Type en X-formbase-Signature — zodat je kunt deduplicaren en routeren voordat je gaat parsen.

De handtekening verifiëren

Elke callback bevat een handtekeningheader, X-formbase-Signature: t={unix seconden},sha256={hex}. De hex is een HMAC-SHA256 van het tijdstempel, een punt, en de ruwe request-body, berekend met het request signing secret van je werkruimte.

Twee regels, welke taal je ook gebruikt:

  1. Hash de ruwe body, voordat er iets geparseerd of opnieuw geserialiseerd wordt. Opnieuw gecodeerde JSON is niet dezelfde bytes.

  2. Vergelijk in constante tijd — crypto.timingSafeEqual, hmac.compare_digest — nooit met ==.

Het request signing secret

Eén geheim per werkruimte ondertekent elke callback ervan. Vind het bij OAuth en API-sleutels in de zijbalk van de werkruimte, op de kaart Request signing secret. Het is standaard verborgen; Geheim tonen toont het en de kopieerknop kopieert het. Het is geen eenmalige waarde — je kunt terugkomen en het opnieuw lezen. Het geheim wordt aangemaakt de eerste keer dat het nodig is, dus een werkruimte die die kaart nog nooit heeft geopend en nog nooit een aanvraag met een callbackUrl heeft aangemaakt, heeft er nog geen.

Nieuwe pogingen

Een callback krijgt acht pogingen: de eerste, dan zeven nieuwe pogingen met minstens 1, 2, 4, 8, 16, 32 en 60 minuten ertussen. formbase zoekt elke 30 minuten naar nieuwe pogingen die klaarstaan, dus een nieuwe poging kan tot een half uur na het einde van zijn tussenpoos komen, en de laatste poging komt zo’n vier uur na het einde van de aanvraag. Elke poging bevat dezelfde bytes en dezelfde id: de payload wordt bevroren op het moment dat de aanvraag eindigde, dus een nieuwe poging beschrijft wat er toen gebeurde, niet hoe de aanvraag er nu uitziet. De bestemmings-URL en het signing secret worden bij elke poging opnieuw gelezen, niet bevroren samen met de payload.

Jouw responsWat formbase doet
2xxKlaar. De callback wordt gemarkeerd als afgeleverd.
408, 429, 5xxProbeert opnieuw, met inachtneming van Retry-After als je die meestuurt.
Andere 4xxStopt. Je endpoint heeft de aanroep afgewezen; dezelfde body opnieuw sturen kan niet helpen.
Timeout of verbindingsfoutProbeert opnieuw volgens hetzelfde schema.
Geblokkeerde URLStopt onmiddellijk. Een host die niet oplost, een privéadres, of een niet-HTTPS-URL kan nooit worden toegestaan. Omleidingen worden nooit gevolgd, dus een 3xx stopt ook.

Als het budget opraakt — je ontvanger lag de hele middag plat — is de callback niet verloren. De aanvraag krijgt een badge Callback mislukt, de eigenaar van de werkruimte krijgt eenmalig een e-mail met de host, de reden en het aantal pogingen, en de antwoorden blijven leesbaar via requests.get. Om hem opnieuw te versturen, open je de aanvraag op de pagina Aanvragen en druk je op Opnieuw afspelen, of roep je requests.replayCallback aan. Dat stuurt dezelfde bevroren payload opnieuw met dezelfde id, precies wat een ontvanger die dedupliceert nodig heeft.

Abonnementen ontvangen dezelfde gebeurtenissen

Een callback-URL hoort bij één aanvraag. Wanneer elke aanvraag op een formulier dezelfde ontvanger moet bereiken, abonneer je je in plaats daarvan één keer: de formbase-apps voor Zapier en n8n doen dit voor je, en webhooks.create doet het vanuit code met request_completed, request_expired of request_canceled als gebeurtenistype. Een abonnement ontvangt dezelfde envelop, ondertekend met zijn eigen geheim in plaats van het request signing secret van de werkruimte, met een eigen event-id en een eigen budget aan nieuwe pogingen. Een aanvraag met zowel een callback-URL als een bijpassend abonnement vuurt twee keer af, één keer naar elk. Opnieuw afspelen stuurt alleen de callback opnieuw; een abonnement probeert het zelf opnieuw en pauzeert na vijf mislukte pogingen.

Een resume-URL is geen authenticatie

Workflowtools geven je een moeilijk te raden resume-URL en het is verleidelijk dat als bewijs te behandelen. Het is een bearer-geheim — het kan lekken in logs, en het vertelt je niet dat de body niet is aangepast. Verifieer de handtekening ook in de hervatte tak.