Webhooks

Webhooks

Webhooks are how Shop.mn and PickPack notify each other of state changes. Both sides expose a receiver URL that accepts signed POST requests carrying a JSON event envelope.

Delivery model#

Every state change emits exactly one event. Events are delivered via HTTPS POST to the receiver URL registered during onboarding. The receiver must respond with HTTP 2xx within 10 seconds; any other response (or a timeout) triggers a retry.

Retries use exponential backoff over 24 hours: 1m, 5m, 15m, 1h, 6h, 24h. After the final attempt the event is moved to a dead-letter queue and an alert is raised internally.

Event envelope#

All events share the same outer shape:

json
{
  "id": "evt_...",
  "type": "shipment.picked_up",
  "created_at": "2026-05-26T09:14:22Z",
  "data": { /* event-specific payload */ }
}

The id is unique per event and stable across retries — use it for de-duplication on the receiver side.

Event types#

TypeDescription
shipment.createdEmitted by Shop.mn when a new shipment is registered (the inverse acknowledgement of POST /shipments).
shipment.picked_upEmitted by PickPack when a courier collects the parcel from the vendor.
shipment.in_transitEmitted by PickPack on hub scans and movement events.
shipment.out_for_deliveryEmitted by PickPack when the parcel is on its final leg to the customer.
shipment.deliveredTerminal — emitted by PickPack on successful delivery to the customer.
shipment.failedTerminal — emitted by PickPack when delivery cannot be completed.
shipment.canceledTerminal — emitted by Shop.mn when the vendor cancels the underlying order before pickup.

Signature verification#

Every webhook request includes an X-Shop-MN header. The header value is the HMAC-SHA256 of the raw request body, signed with a shared webhook secret, encoded as lowercase hex.

javascript
import { createHmac, timingSafeEqual } from "node:crypto"

export function verifyShopMnSignature(rawBody, header, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex")
  const a = Buffer.from(expected, "hex")
  const b = Buffer.from(header || "", "hex")
  return a.length === b.length && timingSafeEqual(a, b)
}

Always verify before parsing the body as JSON. Reject requests with an invalid signature with HTTP 401 — never 200, or the dispatcher will treat the event as accepted and stop retrying.

Header name is X-Shop-MN
Note the unusual header name. The industry-default X-Webhook-Signature is not used.