Analyseren

Ondertekende afleveringen, en een e-mail die niets zegt.

Een webhook stuurt elk nieuw antwoord naar een https-adres van uw keuze. Elke aflevering draagt een X-NumoForms-Signature-header: sha256= gevolgd door een HMAC-SHA256 van de exacte inhoud van het verzoek, berekend met een geheim dat alleen bij die webhook hoort, zodat uw eindpunt kan vaststellen dat de gegevens van ons komen. Los daarvan kunnen adressen naar keuze een melding krijgen wanneer er een antwoord binnenkomt — en die e-mails bevatten met opzet geen antwoorden.

Wat er bij uw eindpunt aankomt

Eén JSON-POST per antwoord. De inhoud bevat vijf velden en verder niets, zodat u er een verwerker tegenaan kunt schrijven zonder te gokken.

VeldWat erin staat
eventresponse.created
survey_idDe enquête waar het antwoord bij hoort.
response_idHet unieke kenmerk van dit antwoord.
created_atWanneer het antwoord is vastgelegd.
answersDe antwoorden, gerangschikt op vraagkenmerk.
HeaderWaar hij voor is
X-NumoForms-EventDe naam van de gebeurtenis. Vandaag is er één: response.created.
X-NumoForms-Signaturesha256= gevolgd door de HMAC-SHA256 van de exacte inhoud van het verzoek, in hexadecimale notatie.
X-NumoForms-DeliveryEen uniek kenmerk voor deze afleverpoging, zodat u een dubbele aflevering herkent.

De aflevering wordt in gang gezet door een databasetrigger op de antwoordentabel, niet door een bepaald scherm in de applicatie. Dat is belangrijker dan het klinkt: een antwoord dat via een ingesloten formulier binnenkomt, via een gehoste link, of als een gedeeltelijk antwoord dat dagen later met opslaan en verdergaan wordt hervat, volgt allemaal dezelfde weg, dus er is geen route die we vergeten zijn aan te sluiten.

De webhook vertrekt wanneer het antwoord binnenkomt. Hebt u goedkeuringen aangezet, dan wacht hij niet op het besluit tot goedkeuren of afwijzen, en er wordt ook geen tweede aflevering verstuurd zodra dat besluit valt. Behandel binnenkomende gegevens als „er bestaat een antwoord”, niet als „een antwoord is gefiatteerd”.

Waarom de ondertekening niet optioneel is

Een webhook-adres is een geheim dat lekt. Het belandt in een ticket, in een Slack-bericht, in een schermafbeelding in een overdrachtsdocument, in de browsergeschiedenis van een gedeelde laptop. Zonder ondertekening kan iedereen die het adres heeft gezien er sturen wat hij wil, en uw systemen boeken dat weg als een echt consultatieantwoord. Voor een gemeente die bezwaren tegen een bouwaanvraag telt, is dat geen theoretisch probleem.

Daarom wordt elke aflevering ondertekend. Elke webhook krijgt een eigen, willekeurig gegenereerd geheim, dat in de editor achter een knop „ondertekengeheim tonen” zichtbaar wordt. Wij berekenen een HMAC-SHA256 over de exacte bytes van de inhoud van het verzoek met dat geheim; u berekent hem opnieuw met uw eigen kopie en vergelijkt. Een vervalst verzoek valt af, omdat de vervalser het geheim niet heeft.

Het adres moet https zijn. Dat wordt afgedwongen door een constraint in de database en niet door een aanwijzing in het formulier, omdat een ondertekengeheim dat over een onversleutelde verbinding reist geen geheim is.

Een aflevering controleren in Node.js

Twee details richten in de praktijk de meeste schade aan. Gebruik de onbewerkte inhoud van het verzoek en geen object dat is geparseerd en opnieuw is geserialiseerd — verschillen in witruimte veranderen de hash. En vergelijk met constante looptijd, zodat een aanvaller de juiste ondertekening niet byte voor byte kan afleiden uit de reactietijden.

import { createHmac, timingSafeEqual } from 'node:crypto';

const SECRET = process.env.NOISSIME_WEBHOOK_SECRET;

// rawBody must be the exact bytes received, before any JSON parsing.
// Re-serialising a parsed object changes the whitespace and the check fails.
function isFromNoissime(rawBody, header) {
  if (typeof header !== 'string') return false;

  const expected =
    'sha256=' + createHmac('sha256', SECRET).update(rawBody).digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(header);

  // timingSafeEqual throws on a length mismatch, so compare lengths first.
  return a.length === b.length && timingSafeEqual(a, b);
}

Roep dat aan voordat u iets parseert. Geeft het false terug, antwoord dan met 401 en gooi het verzoek weg. Geeft het true terug, dan hebt u een antwoord dat alleen van uw enquête afkomstig kan zijn.

Het afleverlogboek

Een webhook die stilletjes is opgehouden met werken, ziet er precies zo uit als een webhook die nooit is aangeroepen: er gebeurt niets, en niets vertelt u dat. Daarom wordt elke poging in een afleverlogboek geschreven en staat de laatste HTTP-status bij de webhook in de editor — groen bij een 2xx, rood bij al het andere.

De aflevering verloopt asynchroon, dus de status wordt achteraf vastgesteld in plaats van op het moment van verzenden geraden. Wat u ziet, is de status die uw server werkelijk teruggaf. Elke webhook kan worden gepauzeerd en hervat zonder hem te verwijderen, wat doorgaans is wat u wilt terwijl het ontvangende systeem opnieuw wordt uitgerold.

Eén bewuste beperking: er is geen herhaling met exponentieel oplopende wachttijd. Een aflevering wordt één keer geprobeerd, de uitkomst wordt genoteerd, en daarmee is het klaar. Een herhalingswachtrij bouwen die volgorde en idempotentie respecteert is echt werk, en er een claimen die we niet gebouwd hebben zou erger zijn dan dit gewoon zeggen. Intussen gaat het antwoord nooit verloren — het staat in de database, is zichtbaar in analyse en rapportage, en is als CSV te exporteren.

Een falende webhook kan het verzamelen ook niet stukmaken. Loopt de aflevering op een fout, dan wordt die fout genoteerd en wordt het antwoord alsnog vastgelegd. Het antwoord van de respondent is wat telt; de aflevering is een beste poging.

E-mailmeldingen, zonder de antwoorden

Voeg de adressen toe die over nieuwe antwoorden moeten horen, en zij krijgen een e-mail zodra er een binnenkomt. Het bericht is opgemaakte HTML met een alternatief in platte tekst, omdat zowel tekstprogramma's als spamfilters het platte deel lezen. Het noemt de enquête, geeft de lopende telling en verwijst naar de resultaten.

Wat er niet in staat, zijn de antwoorden. Dat is een besluit, geen omissie. Een meldingsmail wordt doorgestuurd naar een collega, automatisch weggeschreven in een gedeelde mailbox, gesynchroniseerd met een telefoon en jarenlang in een back-up bewaard. Vrije antwoorden uit een consultatie in die keten stoppen, verspreidt persoonsgegevens over systemen die niemand heeft beoordeeld, en ondermijnt stilletjes de toegangscontroles die op de pagina over beveiliging staan beschreven. Gegevens van respondenten blijven in de database, en de e-mail zegt u dat u even moet gaan kijken.

Net als webhooks worden meldingen in gang gezet door een databasetrigger, dus ze volgen het antwoord in plaats van een bepaalde route van indienen. Zijn er geen adressen ingesteld, dan wordt er niets verstuurd en niets geprobeerd.

Wat dit niet doet

Veelgestelde vragen

Hoe controleer ik de X-NumoForms-Signature-header?

Bereken een HMAC-SHA256 over de onbewerkte inhoud van het verzoek met het ondertekengeheim van die webhook, codeer die hexadecimaal, zet er „sha256=” voor en vergelijk het resultaat met de header via een vergelijking met constante looptijd. Controleer vóórdat u de JSON parseert en weiger alles wat niet overeenkomt met een 401.

Wat gebeurt er als mijn eindpunt plat ligt op het moment dat een antwoord binnenkomt?

De aflevering wordt één keer geprobeerd en de uitkomst wordt met de HTTP-status in het afleverlogboek genoteerd. Er is geen automatische herhaling met oplopende wachttijd. Het antwoord zelf loopt nooit gevaar: een mislukte webhook kan niet verhinderen dat een antwoord wordt opgeslagen, en u kunt de gegevens altijd terughalen uit het resultatenscherm of een CSV-export.

Vertrekt er ook een webhook bij een antwoord uit een ingesloten formulier?

Ja. De aflevering wordt in gang gezet door een databasetrigger op de antwoordentabel en niet door een bepaald scherm van de applicatie, dus het gebeurt ongeacht waar een antwoord vandaan komt — een gehoste link, een ingesloten formulier, of een hervat gedeeltelijk antwoord dat zojuist is afgemaakt.

Kan ik een webhook naar een http://-adres sturen?

Nee. Een check constraint in de database vereist dat het adres met https:// begint, en de editor weigert al het andere. Een ondertekengeheim dat over een onversleutelde verbinding wordt verstuurd, is geen geheim.

Bevatten de meldingsmails de antwoorden van de respondent?

Nee, en dat is bewust. De e-mail meldt dat er een antwoord binnen is, noemt de enquête, geeft de lopende telling en verwijst naar de resultaten. Meldingen worden doorgestuurd en blijven jarenlang in back-ups van mailboxen staan, dus gegevens van respondenten blijven in de database, waar de toegangscontroles zitten.

Is er een Zapier-app of een koppeling met Google Sheets?

Nee. Webhooks zijn vandaag de enige uitgaande koppeling. Er is geen Zapier-app en geen connector voor Google Sheets, en voor geen van beide is een datum aangekondigd.

Koppel een enquête aan uw eigen systemen.

Richt een webhook op een https-eindpunt en elk antwoord komt daar ondertekend aan, zodat uw ontvanger kan vaststellen dat het van ons komt en niet van wie het adres ook maar vond. Voeg de adressen toe die een e-mail moeten krijgen zodra er een antwoord binnenkomt, en de antwoorden blijven in de database, waar de toegangscontroles zitten.

  • HMAC-SHA256-ondertekening op elke aflevering
  • Afleverlogboek met de laatste HTTP-status
  • In gang gezet door een databasetrigger, ongeacht de route naar binnen
Begin met bouwen