Webhooks
Configure a webhook in Integrations in the tenant administration. EasyPick requires a publicly reachable HTTPS URL and a 2xx response.
Event types
Section titled “Event types”reserved, loaded, picked_up, expired, cancelled, fault, removed, and door_available.
A webhook event is not the same thing as a shipment status. The payload therefore includes separate status and compartment_state fields.
| Event | Meaning | Shipment status after the event | Typical compartment_state |
|---|---|---|---|
reserved |
A compartment reservation was created. | reserved |
reserved |
loaded |
Loading is confirmed and the protection period has ended. | loaded |
occupied |
picked_up |
The customer completed pickup. | picked_up |
cooldown |
cancelled |
The shipment was cancelled. | cancelled |
reserved for an empty reservation, otherwise awaiting_removal; only the following door_available announces release |
expired |
The reservation or pickup deadline expired. | expired |
reserved for an empty reservation, otherwise awaiting_removal; only the following door_available announces release |
fault |
The shipment could not be completed safely. | fault |
usually awaiting_removal |
removed |
An operator physically removed the contents and confirmed an empty compartment. | unchanged (cancelled, expired, or fault) |
awaiting_removal; the following door_available announces actual release |
door_available |
The compartment is actually released and can be reserved again. | unchanged | available |
The loaded event is the customer notification point: it is delivered only after the loading protection period and after the door has been confirmed closed. If the shipment moves to cancelled, expired, or fault before actual delivery, the pending loaded webhook is suppressed so the e-shop cannot send an invalid pickup notification. removed and door_available are operational compartment events, not additional shipment statuses. door_available is the canonical event after every actual release: after cooldown, after removed, and after immediately releasing a reservation that was never loaded. The payload never contains a PIN.
{ "api_version": "1", "id": "event_01k3example", "type": "loaded", "occurred_at": "2026-08-02T10:04:12Z", "data": { "shipment_id": "shipment_01k3example", "external_order_id": "ORDER-2026-1042", "attempt_number": 1, "retry_of_shipment_id": null, "box_id": "box_01k3example", "box_name": "Prague 1 Box", "compartment_code": "A2", "status": "loaded", "compartment_state": "occupied", "pickup_available_at": "2026-08-02T10:04:12Z", "pickup_expires_at": "2026-08-05T10:02:12Z" }}An integration may treat the compartment as reusable only when compartment_state=available. A terminal status alone does not confirm compartment availability.
attempt_number starts at 1. A retry after a fault has a higher number, and retry_of_shipment_id points to the immediately preceding faulted attempt.
Verify the signature
Section titled “Verify the signature”EasyPick sends:
EasyPick-Event-Id: stable ID for deduplication,EasyPick-Timestamp: signature Unix timestamp,EasyPick-Signature:v1=followed by a hexadecimal HMAC-SHA256.
The signed value is exactly timestamp + "." + raw_request_body. Do not parse and re-serialize JSON before verification.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyEasyPickWebhook(rawBody, headers, secret) { const timestamp = headers['easypick-timestamp']; const received = headers['easypick-signature']?.replace(/^v1=/, ''); if (!timestamp || !received) return false; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = createHmac('sha256', secret) .update(`${timestamp}.`) .update(rawBody) .digest('hex'); const a = Buffer.from(expected, 'hex'); const b = Buffer.from(received, 'hex'); return a.length === b.length && timingSafeEqual(a, b);}<?phpfunction verifyEasyPickWebhook(string $rawBody, string $timestamp, string $signature, string $secret): bool { if (abs(time() - (int) $timestamp) > 300) return false; $received = preg_replace('/^v1=/', '', $signature); $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret); return is_string($received) && hash_equals($expected, $received);}At-least-once delivery
Section titled “At-least-once delivery”The same event may arrive more than once. First store its id atomically, acknowledge duplicate IDs with 2xx without processing them again, and only then update the order. Following failures, EasyPick retries after approximately 1 min, 5 min, 30 min, 2 h, and 12 h.
Keep the HTTP handler short. After signature verification and durable event storage, return 2xx and execute further business logic asynchronously.