Menu

Webhooks ontvangen

In plaats van te pollen laat je Picked vertellen wanneer er iets gebeurt: een order komt binnen, is gepickt of verzonden, of de voorraad van een product verandert. Picked POST binnen enkele seconden een ondertekende JSON-gebeurtenis naar je URL, en blijft het ongeveer een dag proberen als je endpoint plat ligt.

Een endpoint toevoegen

Open in Picked Instellingen → Ontwikkelaars → Webhooks (je hebt het recht webhooks.manage nodig, dat eigenaren en beheerders hebben) en voeg je URL en de gebeurtenissen toe die hij moet krijgen.

  • De URL moet openbaar https zijn. Picked roept nooit interne of privé-adressen aan, controleert dat opnieuw voor elke levering, en volgt geen redirects: antwoord op de URL zelf.
  • Picked toont het ondertekeningsgeheim van het endpoint (whsec_…) één keer. Bewaar het; je hebt het nodig om gebeurtenissen te controleren. Geheim vernieuwen geeft je een nieuw.
  • Testgebeurtenis sturen levert een ping-gebeurtenis af en toont het antwoord van je endpoint.

Gebeurtenissen

Gebeurtenis Wanneer
order.imported Er kwam een nieuwe order binnen als werk, via welk kanaal ook (je webshop of ERP, de API, een CSV, of met de hand). Een order die al verzonden of geannuleerd binnenkomt, wordt niet gemeld.
order.picked Het picken van de order is klaar.
order.ready_for_pickup Een afhaalorder ligt klaar om op te halen.
order.shipped De order heeft het pand verlaten: de zending is aan de vervoerder overgedragen, of een afhaal- of eigen-bezorgorder is overhandigd.
order.cancelled De order is geannuleerd.
stock.changed De verkoopbare of beschikbare voorraad van een product in een magazijn veranderde.

Wat je ontvangt

Elke gebeurtenis is een POST met een JSON-body. data is precies wat de bijbehorende lees-aanroep teruggeeft: GET /api/v1/orders/{order} voor ordergebeurtenissen, het voorraadniveau dat GET /api/v1/stock voor die SKU teruggeeft voor stock.changed. Hij wordt opgebouwd op het moment van de gebeurtenis, dus een nieuwe poging levert af wat er gebeurde, niet de stand bij aflevering.

{
    "id": "evt_01JAB3XK8Q2V7N0R5T9W4YH6CD",
    "type": "order.shipped",
    "occurred_at": "2026-10-17T14:02:11Z",
    "data": {
        "id": 48211,
        "external_id": "WS-2026-104233",
        "status": "shipped",
        "fulfilment_method": "carrier",
        "shipments": [
            { "carrier": "postnl", "tracking_number": "3SPCKD0104233" }
        ],
        "_links": { "self": { "href": "/api/v1/orders/48211" } }
    },
    "_links": { "self": { "href": "/api/v1/orders/48211" } }
}

De headers volgen de Standard Webhooks-specificatie:

Header
webhook-id De id van de gebeurtenis. Gelijk bij elke nieuwe poging en bij opnieuw leveren, dus gebruik hem om een gebeurtenis te negeren die je al verwerkte.
webhook-timestamp Unix-seconden van deze poging. Weiger een gebeurtenis die veel ouder is dan een paar minuten.
webhook-signature v1, en een base64 HMAC-SHA256 van {webhook-id}.{webhook-timestamp}.{body}, met als sleutel het base64-gedecodeerde deel van je geheim na whsec_.

Antwoord binnen 10 seconden met een 2xx-status, en doe het langzame werk daarna. Picked bewaart de eerste 1 KB van je antwoord voor de lijst met leveringen, en geeft het op bij een antwoord van meer dan 1 MB.

De handtekening controleren

Gebruik een Standard Webhooks-bibliotheek (PHP, Node, C# en meer), of controleer hem zelf. Gebruik altijd de ruwe request-body, vóór het parsen van de JSON.

<?php
$secret = getenv('PICKED_WEBHOOK_SECRET'); // whsec_…
$body = file_get_contents('php://input');
$id = $_SERVER['HTTP_WEBHOOK_ID'];
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'];

$key = base64_decode(substr($secret, strlen('whsec_')));
$expected = 'v1,'.base64_encode(hash_hmac('sha256', "{$id}.{$timestamp}.{$body}", $key, true));
$valid = false;
foreach (explode(' ', $_SERVER['HTTP_WEBHOOK_SIGNATURE']) as $given) {
    $valid = $valid || hash_equals($expected, $given);
}
$valid = $valid && abs(time() - (int) $timestamp) <= 300;

http_response_code($valid ? 204 : 401);
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(secret, headers, rawBody) {
    const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
    const id = headers['webhook-id'];
    const timestamp = headers['webhook-timestamp'];
    const expected = createHmac('sha256', key)
        .update(`${id}.${timestamp}.${rawBody}`)
        .digest('base64');

    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
        return false;
    }

    return headers['webhook-signature'].split(' ').some((signature) => {
        const given = Buffer.from(signature.replace(/^v1,/, ''));
        const wanted = Buffer.from(expected);

        return given.length === wanted.length && timingSafeEqual(given, wanted);
    });
}
using System.Security.Cryptography;
using System.Text;

static bool Verify(string secret, string id, string timestamp, string signatures, string rawBody)
{
    var key = Convert.FromBase64String(secret["whsec_".Length..]);
    using var hmac = new HMACSHA256(key);
    var expected = "v1," + Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{id}.{timestamp}.{rawBody}")));

    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(timestamp)) > 300) return false;

    return signatures.Split(' ').Any(s => CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(s), Encoding.UTF8.GetBytes(expected)));
}

Nieuwe pogingen en uitzetten

  • Een levering zonder 2xx wordt opnieuw geprobeerd na 1 minuut, 5 minuten, 30 minuten, 1 uur, 2 uur, 6 uur en 12 uur: 8 pogingen in ongeveer 22 uur. Elke poging staat 30 dagen met statuscode en het begin van je antwoord in de lijst Leveringen van het endpoint.
  • Opnieuw leveren in die lijst stuurt dezelfde gebeurtenis nog eens, met dezelfde webhook-id.
  • Een endpoint zet zichzelf uit, en wie webhooks beheert krijgt bericht, als het 410 Gone antwoordt, als het adres naar een intern netwerk gaat wijzen, of als de leveringen 50 keer achter elkaar of 3 dagen lang mislukken. Los het op en druk op Weer aanzetten.
  • Gebeurtenissen kunnen meer dan eens aankomen en, over nieuwe pogingen heen, in een andere volgorde. Gebruik webhook-id om dubbele te negeren en occurred_at om te ordenen; lees bij twijfel de actuele stand met de _links.self-URL.