← Blog

Integrations

Shopify webhook-logs: app-fout vs netwerkfout herkennen

Vier patronen in een Shopify webhook-log scheiden een wankele integratie van een wankel netwerk. Lees ze één keer en je wijst nooit meer de verkeerde laag aan.

Jacob Molkenboer· Oprichter · A Brand New Company· 7 jun 2024· 6 min
Twee koperen posttags verbonden met waxdraad naast een gevouwen telegram met groen lint op ivoren bureau.

Maandagochtend. De fulfilment-lead belt. Bestellingen verschijnen dubbel in hun magazijnsoftware. Soms helemaal niet. Shopify-support antwoordt met de zin die elke integratie-engineer vreest: "de webhook is bij ons succesvol afgeleverd." Jouw team antwoordt met de spiegelversie: "wij hebben hem nooit ontvangen." Allebei meestal waar. Het gevecht gaat over wiens log fout zit.

Elke Shopify-integratie heeft dit gesprek minstens één keer. Wij hebben het inmiddels een stuk of tien keer gevoerd met klanten op woninginrichting-storefronts, subscription-apps en middleware tussen Shopify en een ERP. Het goede nieuws: het antwoord staat bijna altijd al in de webhook-log. Je moet alleen weten waar je kijkt.

Dit zijn de vier patronen waar wij als eerste naar zoeken. Geen ervan vereist nieuwe tools. Ze zitten in de headers en timestamps die je al vastlegt.

De headers waar de diagnose in zit

Shopify hangt aan elke webhook een handvol headers die je uit je hoofd wil kennen. De headers die er voor triage toe doen:

  • X-Shopify-Webhook-Id: een UUID die stabiel blijft over alle retries van hetzelfde event.
  • X-Shopify-Triggered-At: ISO-8601 timestamp van het moment waarop het event aan Shopify-kant afging.
  • X-Shopify-Topic: bijvoorbeeld orders/create, orders/updated.
  • X-Shopify-Hmac-Sha256: base64 HMAC over de raw request body.
  • X-Shopify-Api-Version: handig als gedrag verandert na een API-upgrade.

Als je log die vijf vastlegt plus je eigen aankomst-timestamp, de response-code die je teruggaf en de eerste 200 bytes body, heb je alles wat triage nodig heeft.

Patroon 1: de retry-ladder

Shopify doet tot 19 retries verspreid over 48 uur op gefaalde webhooks, met steeds langere tussenpozen. Grep één enkele X-Shopify-Webhook-Id en je hoort één rij te zien. Zie je een ladder van pogingen 1, 2, 4, 8, 15 minuten uit elkaar, dan zegt Shopify tegen je dat je endpoint non-2xx teruggaf.

De vorm van de ladder vertelt welk soort fout het is. Een reeks die eindigt met een schone 200 betekent dat je app uiteindelijk herstelde, meestal na een deploy of nadat een downstream service terugkwam. Een reeks die negentien sporten klimt en steeds met een 500 stopt, betekent een logica-fout die reproduceerbaar is op de payload.

grep "X-Shopify-Webhook-Id: 4b...e2" access.log | awk '{print $1, $9}'
2026-06-02T09:14:01Z 500
2026-06-02T09:15:03Z 500
2026-06-02T09:17:08Z 500
2026-06-02T09:21:14Z 500
2026-06-02T09:29:31Z 200

Die laatste 200 is degene om te onderzoeken. Wat veranderde tussen 09:14 en 09:29? Een deploy? Een cache-warm-up? Een foreign key die eindelijk bestond omdat de bijbehorende customers/create-webhook was binnengekomen?

Patroon 2: de HMAC-drift

HMAC-fouten komen in twee smaken. De saaie is een verkeerde shared secret, die honderd procent van de tijd faalt vanaf het moment dat je hem inschakelt. De interessante is intermitterend: de meeste webhooks verifiëren, maar een deel niet, en de mislukkingen clusteren rond een deploy of een middleware-wijziging.

Intermitterende HMAC-fouten liggen bijna nooit aan Shopify. Bijna altijd is het je eigen stack die de request body muteert voordat je de signature checkt. De gebruikelijke verdachten, op volgorde van hoe vaak we ze tegenkomen:

  1. Een JSON body parser die vóór de verifier draait, waardoor je een opnieuw-geserialiseerde body krijgt die niet meer matcht met de bytes die Shopify hashte.
  2. Een reverse proxy die newlines herschrijft of een trailing byte afsnijdt.
  3. Een emoji of ander multibyte karakter in een klantnaam, ergens in de keten gedecodeerd als verkeerde charset.

De oplossing is altijd dezelfde: verifieer tegen de raw bytes, voordat een parser ze aanraakt. In Express ziet dat er zo uit:

app.post(
  '/webhooks/shopify',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const hmac = req.get('X-Shopify-Hmac-Sha256');
    const digest = crypto
      .createHmac('sha256', process.env.SHOPIFY_WEBHOOK_SECRET)
      .update(req.body) // Buffer, not string
      .digest('base64');
    if (digest !== hmac) return res.status(401).end();
    const event = JSON.parse(req.body.toString('utf8'));
    // ... handle event
    res.status(200).end();
  }
);
Waarschuwing

Als de body parser van je framework globaal is geregistreerd, ziet de verifier een opnieuw-geserialiseerde string en faalt elke HMAC. Mount de raw parser alleen op de webhook-route, vóór alle globale JSON-middleware.

Patroon 3: het stille gat

Dit is degene die de meeste teams verkeerd diagnostiseren. Een merchant klaagt dat orders missen. Je app-log heeft geen spoor van de webhook. Shopify's webhook-dashboard toont hem als afgeleverd met een 200 response code. Beide kanten zijn er zeker van dat ze gelijk hebben.

Hier betekent "delivered" dat Shopify's TCP-laag een 200 kreeg van iets. Dat iets kan je CDN-edge zijn, je load balancer, een stale Heroku-router of een Cloudflare worker die de request opslokte en nooit doorzette. De diagnose is om drie logs naast elkaar te leggen: Shopify's delivery-log, je edge of load-balancer-log en je app-log. De laag waar het gat opengaat, is de kapotte laag.

Netwerkgaten hebben een herkenbare vorm. Ze clusteren in de tijd (één slecht venster van vijf minuten), ze raken elk topic gelijk en ze laten geen enkel spoor achter aan je app-kant. Applicatiegaten raken een specifiek topic of een specifieke payload-vorm, en je app-log toont dat de request binnenkomt voordat er iets misgaat.

Patroon 4: de scheefgroei

Bereken voor elke webhook received_at min X-Shopify-Triggered-At en zet het uit op een grafiek. Bij een gezonde integratie is de lijn een vlakke band onder vijf seconden, met af en toe een piek. Alles anders is een diagnose.

Een lijn die over uren omhoog kruipt betekent dat je app trager verwerkt dan Shopify verzendt, en dat Shopify's queue voor jouw endpoint volloopt. De oplossing is óf je handler versnellen, óf de webhook binnen een seconde ack'en en asynchroon verwerken. Shopify is bot over de deadline: antwoord binnen vijf seconden of je riskeert als unhealthy gemarkeerd te worden en uiteindelijk uitgeschreven.

Een lijn die alleen op specifieke topics piekt, wijst op een downstream dependency die traag is voor die payloads, zoals een voorraad-lookup, een btw-berekening of een externe ERP-call. Een lijn die op elk topic piekt, wijst op je eigen infrastructuur.

Wat dit verandert aan het gesprek

Dat deze vier patronen ertoe doen is geen technische kwestie, maar een politieke. Als een merchant orders verliest, is een "het ligt niet aan ons"-antwoord dat geen specifieke logregel kan aanwijzen helemaal niets waard. Een antwoord dat zegt "jouw TCP-laag accepteerde de webhook maar onze app heeft de request nooit gezien, hier zijn de drie log-timestamps die dat bewijzen" eindigt de discussie en verkort de storing.

Toen wij de order-sync-laag bouwden voor een Nederlandse woninginrichting-merchant, ving de viercheck hierboven een Cloudflare worker die 200 teruggaf aan Shopify en stilzwijgend de body dropte op elke webhook boven 64KB, een config-drift die negen dagen lang door niemand was opgemerkt. Hetzelfde draaiboek past op elke webhook-gedreven integratie die wij bouwen.

Het kleinste wat je vandaag kunt doen: voeg X-Shopify-Webhook-Id, X-Shopify-Triggered-At en je eigen received_at toe aan wat je app al logt. Vijf minuten werk. De volgende keer dat de vraag binnenkomt, heb je het antwoord voordat het gesprek eindigt.

Kern

Vier headers en één timestamp-delta vertellen je of een ontbrekende Shopify-webhook de schuld is van je app of van je netwerk.

FAQ

Hoe lang blijft Shopify een gefaalde webhook retryen?

Tot 19 pogingen verspreid over 48 uur, met steeds langere tussenpozen. Na 48 uur falen kan Shopify het topic als unhealthy markeren en je endpoint uiteindelijk uitschrijven.

Waarom falen mijn HMAC-checks intermitterend?

Bijna altijd omdat een body parser vóór de verifier draait en de payload opnieuw serialiseert. Verifieer tegen de raw request bytes, voordat enige JSON-middleware de request aanraakt.

Moet ik een webhook bevestigen vóór of na het verwerken?

Vóór. Geef binnen vijf seconden een 200 terug en verwerk asynchroon. Shopify markeert trage endpoints als unhealthy en stopt dan met events sturen.

Shopify zegt afgeleverd, mijn app heeft het nooit gezien. Wat nu?

Leg drie logs naast elkaar: Shopify's delivery-log, je edge of load-balancer-log en je app-log. De laag waar het gat als eerste opengaat, is de kapotte laag.

integrationse-commerceworkflowtoolingoperationsarchitecture

Iets bouwen?

Start een project