Skip to content

Webhooks

Configure a webhook in Integrations in the tenant administration. EasyPick requires a publicly reachable HTTPS URL and a 2xx response.

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.

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);
}
<?php
function 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);
}

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.