Webhooks: orderupdates automatisch ontvangen
11 min leestijdBijgewerkt
Met webhooks stuurt Printen.eu je een bericht zodra er iets met een order gebeurt: wacht op saldo, in productie, verzonden met tracking, afgeleverd of geannuleerd. Zo hoef je de API niet steeds te bevragen. In dit artikel voeg je een endpoint toe, controleer je de handtekening en bouw je een verwerking die tegen herhalingen kan.
In dit artikel
Hoe werken webhooks?
Je geeft ons een https-url van je eigen server. Bij elke gebeurtenis sturen we daar een POST-verzoek naartoe met een JSON-bericht. Je server antwoordt met een 2xx-status, en dan weten wij dat het bericht is aangekomen.
X-Printen-Event: het soort event, bijvoorbeeldorder.shipped.X-Printen-Delivery: een id van deze aflevering. Handig in je logs.X-Printen-Timestamp: het tijdstip van verzenden, in seconden sinds 1970 (Unix-tijd).X-Printen-Signature: de handtekening (v1=gevolgd door een hex-code), waarmee je controleert dat het bericht echt van ons komt.
Hoe voeg je een endpoint toe?
Open het tabblad
Ga in je dashboard naar Webhooks.
Vul je url in
Vul bij Url (https) het adres van je eigen server in, bijvoorbeeld
https://jouwdomein.nl/printen-webhook.Kies de events
Vink aan welke events je wilt ontvangen. Standaard staan
order.shipped,order.payment_failedenorder.awaiting_fundsaangevinkt.Voeg de webhook toe
Klik op Webhook toevoegen. Je ziet nu het geheim, met de melding Bewaar dit geheim nu. Je ziet het maar één keer.
Bewaar het geheim
Zet het geheim direct in de configuratie van je server, bijvoorbeeld als omgevingsvariabele. Je hebt het nodig om de handtekening te controleren.
Je kunt een endpoint ook via de API toevoegen met POST /webhooks. Laat je events weg of stuur je een lege lijst, dan ontvang je alle events. Het geheim staat alleen in dit ene antwoord.
curl -X POST https://www.printen.eu/api/reseller/v1/webhooks \
-H "Authorization: Bearer prk_live_..." -H "Content-Type: application/json" \
-d '{"url":"https://example.com/printen-webhook","events":["order.shipped","order.delivered"]}'{
"data": {
"id": "WEBHOOK_ID",
"url": "https://example.com/printen-webhook",
"events": ["order.shipped", "order.delivered"],
"allEvents": false,
"active": true,
"createdAt": "2026-10-10T09:00:00.000Z",
"updatedAt": "2026-10-10T09:00:00.000Z",
"secret": "WEBHOOK_SECRET"
}
}- De url moet https gebruiken en publiek bereikbaar zijn. Adressen als
localhost, interne netwerkadressen en namen die eindigen op.local,.internalof.lanworden geweigerd. - Je kunt maximaal 10 endpoints per account hebben.
- Elk endpoint heeft een eigen geheim.
Welke events zijn er?
| Event | Wanneer | Wat je meestal doet |
|---|---|---|
order.created | Een order is aangemaakt, via de API, het dashboard of een gekoppelde winkel. | Ons ordernummer opslaan bij je eigen order. |
order.submitted | De order is betaald en gaat naar productie. | De order in je systeem op "in behandeling" zetten. |
order.awaiting_funds | Je saldo is te laag; de order wacht. | Jezelf waarschuwen en opwaarderen. |
order.payment_failed | De betaling met je opgeslagen kaart is mislukt. | Je kaart controleren en de order opnieuw proberen in het dashboard. |
order.in_production | De productie is begonnen. | De status tonen. Annuleren kan vanaf nu niet meer. |
order.shipped | Het pakket is verzonden; tracking bevat code en link. | De tracking naar je klant sturen of in je winkel zetten. |
order.delivered | Het pakket is afgeleverd. | De order afsluiten. |
order.cancelled | De order is geannuleerd. | De order in je systeem annuleren en een eventuele terugbetaling verwerken. |
order.file_problem | Er is een probleem met het drukbestand. We nemen contact met je op. | Je bestand controleren en op ons bericht reageren. |
Hoe ziet een bericht eruit?
Elk bericht heeft dezelfde opbouw. Hieronder een voorbeeld van order.shipped.
{
"id": "EVENT_ID",
"type": "order.shipped",
"created": "2026-10-10T14:32:05.000Z",
"data": {
"order": {
"id": "ORDER_ID",
"orderNumber": "RS-3F9A1C7B2E",
"externalOrderId": "1001",
"externalOrderNumber": "1001",
"state": "SUBMITTED",
"status": "SHIPPED",
"tracking": {
"code": "3SABCD1234567",
"url": "https://example.com/track/3SABCD1234567"
},
"items": [
{
"productId": "PRODUCT_ID",
"name": "Poster",
"quantity": 1,
"options": { "size": "A2" }
}
]
}
}
}| Veld | Betekenis |
|---|---|
id | Unieke id van het event. Behandel het als een tekst zonder vaste opbouw en gebruik het om dubbele berichten te herkennen. |
type | Het soort event, gelijk aan de header X-Printen-Event. |
created | Wanneer het event is ontstaan (ISO 8601). |
data.order.orderNumber | Ons ordernummer, zoals je het in het dashboard ziet. |
data.order.externalOrderId | Jouw eigen order-id, als je die bij het bestellen hebt meegegeven. |
data.order.state en status | De status aan jouw kant (bijvoorbeeld SUBMITTED) en de productiestatus (bijvoorbeeld SHIPPED). Zie orderstatussen. |
data.order.tracking | Trackingcode en trackinglink, zodra de order verzonden is. |
Extra velden in data | Bij order.awaiting_funds en order.payment_failed staat de reden in reason. Bij order.cancelled kan refundedCents staan. In de sandbox heeft order.submitted het veld test. |
Hoe controleer je de handtekening?
Iedereen kan een verzoek naar je url sturen. Controleer daarom bij elk bericht de handtekening voordat je er iets mee doet. De handtekening is een HMAC-SHA256 met het geheim van je endpoint.
Lees de headers
Haal
X-Printen-TimestampenX-Printen-Signatureuit het verzoek.Controleer de tijd
Wijs het bericht af als de timestamp meer dan 5 minuten (300 seconden) afwijkt van de huidige tijd. Zo kan niemand een oud bericht opnieuw afspelen.
Bereken de verwachte handtekening
Bereken een HMAC-SHA256 met je geheim over de tekst
timestamp.rawBody: de timestamp, een punt en daarna de ruwe body. Zet erv1=voor.Vergelijk veilig
Vergelijk de uitkomst met
X-Printen-Signaturemet een vergelijking die altijd even lang duurt (constant time). Komen ze niet overeen, antwoord dan met 401 en verwerk niets.
import crypto from 'node:crypto';
export function verifyPrintenWebhook(rawBody, headers, secret) {
const timestamp = headers['x-printen-timestamp'];
const signature = headers['x-printen-signature'] || '';
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'v1=' + crypto.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`).digest('hex');
return signature.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}<?php
function verify_printen_webhook(string $rawBody, string $timestamp, string $signature, string $secret): bool {
if (abs(time() - (int) $timestamp) > 300) return false;
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signature);
}import hashlib, hmac, time
def verify_printen_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = "v1=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)Valkuil: controleer op de ruwe body
De handtekening is berekend over de body precies zoals wij hem versturen, byte voor byte. Veel frameworks lezen de body automatisch als JSON. Zet je die JSON daarna weer om naar tekst, dan kunnen spaties, volgorde of tekens net anders zijn, en klopt de handtekening niet meer.
import express from 'express';
import { verifyPrintenWebhook } from './verify-printen-webhook.js';
const app = express();
// express.raw (not express.json) keeps the body exactly as it was sent
app.post('/printen-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const rawBody = req.body.toString('utf8');
if (!verifyPrintenWebhook(rawBody, req.headers, process.env.PRINTEN_WEBHOOK_SECRET)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(rawBody);
await saveEventForProcessing(event); // store first, process later
res.status(200).send('ok');
});- PHP: lees de body met
file_get_contents("php://input"), niet met$_POST. - Python (Flask): gebruik
request.get_data()voor de ruwe bytes, nietrequest.json. - Next.js (App Router): gebruik
await request.text()en pas daarnaJSON.parse.
Wat als je endpoint niet antwoordt?
Een aflevering is geslaagd als je binnen 10 seconden antwoordt met een 2xx-status. Elke andere uitkomst telt als mislukt: een time-out, een foutstatus, maar ook een doorverwijzing (3xx). Dan proberen we het opnieuw volgens dit schema.
| Poging | Wachttijd na de vorige poging | Ongeveer na de eerste poging |
|---|---|---|
| 1 | Direct | Start |
| 2 | 1 minuut | 1 minuut |
| 3 | 5 minuten | 6 minuten |
| 4 | 15 minuten | 21 minuten |
| 5 | 30 minuten | 51 minuten |
| 6 | 1 uur | 2 uur |
| 7 | 2 uur | 4 uur |
| 8 | 4 uur | 8 uur |
| 9 | 8 uur | 16 uur |
| 10 | 8 uur | 24 uur |
Na de tiende poging geven we het voor dat event op. Je endpoint wordt nooit automatisch uitgeschakeld: nieuwe events blijven we gewoon sturen.
Dubbele en ongeordende events verwerken
Door herhalingen kan hetzelfde event meer dan één keer binnenkomen, en events kunnen in een andere volgorde aankomen dan ze zijn ontstaan. Bouw je verwerking daarop.
- Ontdubbel op `id`: bewaar de
idvan elk verwerkt event. Komt dezelfdeidnog eens binnen, antwoord dan met 2xx en doe verder niets. - Let op de volgorde met `created`: heb je al een nieuwer event voor deze order verwerkt, overschrijf dan geen nieuwere status met een oudere.
- Zet geen status terug: komt
order.shippedbinnen naorder.delivered, laat de order dan op afgeleverd staan. - Twijfel je, vraag de order op: met
GET /orders/{id}krijg je altijd de actuele stand.
Hoe test je je webhook?
Zet je endpoint klaar
Zorg dat je url publiek bereikbaar is via https. Werk je lokaal, gebruik dan een tunnelservice die een publieke https-url doorstuurt naar je eigen computer.
Voeg het endpoint toe
Voeg het toe bij Webhooks en vink
order.created,order.submittedenorder.cancelledaan (of laat de events weg via de API om alles te ontvangen).Plaats een sandboxorder
Plaats met een testsleutel (
prk_test_) een order via de API. Binnen een paar minuten ontvang jeorder.createdenorder.submitted(mettestindata).Annuleer de testorder
Annuleer de order met
POST /orders/{id}/cancel. Je ontvangt danorder.cancelled.Controleer je logs
Kijk of de handtekening klopt, of je 2xx teruggaf en of een herhaald bericht met dezelfde
idnetjes werd genegeerd.
Problemen oplossen
Ik ontvang helemaal niets.
Controleer of de url klopt, https gebruikt en vanaf internet bereikbaar is (geen localhost of intern adres). Kijk of je de juiste events hebt aangevinkt. Houd er rekening mee dat een event pas na een paar minuten kan aankomen.
De handtekening klopt nooit.
Controleer of je het geheim van dit endpoint gebruikt (elk endpoint heeft een eigen geheim) en of je op de ruwe body controleert, niet op een opnieuw opgebouwde JSON. Zie de ruwe body.
Ik krijg berichten dubbel.
Dat is normaal na een time-out of foutstatus. Ontdubbel op de id van het event. Zie dubbele events.
Mijn server stuurt een doorverwijzing.
We volgen geen doorverwijzingen; een 301 of 302 telt als mislukt. Gebruik de uiteindelijke url, let daarbij op www, een slash aan het eind en http naar https.
Ik heb het geheim niet bewaard.
Verwijder het endpoint en voeg het opnieuw toe. Je krijgt dan een nieuw geheim; pas dat aan in je serverconfiguratie.
Ik kan geen endpoint meer toevoegen.
Je zit op het maximum van 10 endpoints. Verwijder er eerst een die je niet meer gebruikt.
Gerelateerde artikelen
- De API: bestellen vanuit je eigen systeemMaak een API-sleutel, vraag prijzen op, plaats orders met een Idempotency-Key, beheer je eigen producten en test alles veilig in de sandbox.
- Orders volgen, wijzigen en problemen oplossenVolg je orders, begrijp elke status, plaats handmatig een order, annuleer op tijd en weet wat je doet bij betaalproblemen, adresfouten of schade.
Kom je er niet uit?
We helpen je graag verder, op werkdagen persoonlijk en meestal dezelfde dag.