Webhooks
Register webhook endpoints, verify Svix signatures, and handle Montavire webhook retries safely.
Instead of polling, register an HTTPS endpoint and Montavire will POST an event to
it whenever something changes (a shipment status moves, a supplier is disabled, and
so on). Webhooks are managed under the /api/v1/webhooks resource.
Register an endpoint
curl -X POST https://api.montavire.com/v1/webhooks \
-H "X-API-Key: mv_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "ERP sync",
"url": "https://erp.example.com/hooks/montavire",
"event_types": ["shipment.status_changed", "shipment.tracking_updated"]
}'| Field | Required | Notes |
|---|---|---|
name | yes | A label for the endpoint |
url | yes | Public HTTPS receiver. Internal and private addresses are rejected |
event_types | no | The event types to receive. A webhook with none receives nothing |
description | no | Free text |
The create response returns the signing secret once, as plaintext_secret:
{
"plaintext_secret": "whsec_…",
"webhook": { "id": "…", "name": "ERP sync", "status": "active", "signing_secret_prefix": "whsec_ab" }
}Store plaintext_secret securely. Later reads of the webhook only return
signing_secret_prefix, never the full secret. If you lose it, rotate the webhook.
Event types
Event types follow <resource>.<event>. A webhook belongs to one product and
receives only that product's events. Supply Chain webhooks can subscribe to:
| Event type | Sent when |
|---|---|
po.created | A purchase order is created: in the app, through the API, by an import, or by your ERP posting it to the order webhook |
po.status_changed | A purchase order's status changes |
shipment.created | A shipment is created: in the app, through the API or by an import |
shipment.status_changed | A shipment's status changes |
shipment.tracking_updated | A shipment's tracking details change |
pod.submitted | A proof of delivery is submitted |
supplier.status_changed | A supplier is disabled or enabled again |
Ship webhooks can subscribe to ship.order.received, ship.shipment.labelled
and ship.shipment.shipped.
List the exact types you want in event_types. A webhook with no event types
receives nothing. Each event goes only to your own account's webhooks, and test
keys' events go only to the sandbox's webhooks.
Payloads
Every Supply Chain event carries type, entity_type, entity_id,
tenant_id, occurred_at and, when a person or key made the change, actor.
Status events add status_field, from_status, to_status and reason.
The created events add data, with the record's ids, references and status.
They carry no prices or addresses: read the record with your key when you need
those. A purchase order:
{
"type": "po.created",
"entity_type": "PurchaseOrder",
"entity_id": "0192f0c4-…",
"tenant_id": "…",
"occurred_at": "2026-11-11T09:30:00Z",
"actor": "[email protected]",
"data": {
"id": "0192f0c4-…",
"reference": "PO-ACME-2026-00046",
"po_number": "PO-ACME-2026-00046",
"external_reference": "ERP-12345",
"status": "DRAFT",
"supplier_id": "…",
"supplier_code": "SUP-001",
"factory_id": null,
"order_type": "RETAIL",
"source_system": "MANUAL",
"currency": "GBP",
"order_date": "2026-11-11",
"expected_delivery_date": "2027-01-15",
"line_count": 3,
"created_at": "2026-11-11T09:30:00"
}
}A shipment:
{
"type": "shipment.created",
"entity_type": "Shipment",
"entity_id": "0192f0c9-…",
"tenant_id": "…",
"occurred_at": "2026-11-11T09:35:00Z",
"actor": "[email protected]",
"data": {
"id": "0192f0c9-…",
"reference": "SHP-ACME-2026-00012",
"shipment_number": "SHP-ACME-2026-00012",
"external_id": "ASN-778",
"status": "READY_TO_SHIP",
"purchase_order_id": "0192f0c4-…",
"supplier_company_id": "…",
"carrier": "DHL",
"destination": "London DC",
"estimated_arrival": "2026-12-01",
"total_cartons": 40,
"total_pieces": 960,
"line_count": 2,
"created_at": "2026-11-11T09:35:00"
}
}Verifying signatures (important)
Montavire signs every delivery with the Svix scheme, so you can verify it with
any Svix library. Each
POST carries three headers:
| Header | Meaning |
|---|---|
svix-id | Unique delivery id |
svix-timestamp | Unix seconds when the signature was generated |
svix-signature | Space-separated list of v1,<base64> signatures |
The signature is HMAC-SHA256 over the string {svix-id}.{svix-timestamp}.{body},
keyed on your signing secret, base64-encoded.
Always verify the signature and reject deliveries whose timestamp is too old.
This is how you know a POST genuinely came from Montavire and was not replayed.
Verify with the Svix library (recommended):
import { Webhook } from "svix";
const wh = new Webhook(process.env.MONTAVIRE_WEBHOOK_SECRET); // the whsec_… value
// throws if the signature or timestamp is invalid
const event = wh.verify(rawRequestBody, {
"svix-id": req.headers["svix-id"],
"svix-timestamp": req.headers["svix-timestamp"],
"svix-signature": req.headers["svix-signature"],
});Or compute it yourself:
import crypto from "node:crypto";
const signed = `${svixId}.${svixTimestamp}.${rawBody}`;
const secretBytes = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = crypto.createHmac("sha256", secretBytes).update(signed).digest("base64");
// constant-time compare `expected` against each `v1,<sig>` in svix-signatureRespond 2xx quickly (within a few seconds). Do the real work asynchronously so a
slow downstream never causes a redelivery.
Retries and dead-lettering
If your endpoint does not return 2xx, Montavire retries with backoff. You can
inspect and replay deliveries through the webhook delivery endpoints. Each delivery
record reports:
| Field | Meaning |
|---|---|
attempts | How many times it has been tried |
last_status | The last HTTP status your endpoint returned |
last_error | The last transport or HTTP error |
next_attempt_at | When the next retry is scheduled |
delivered_at | When it finally succeeded |
dead_lettered_at | When it was given up on after exhausting retries |
Use replay to re-send a delivery once you have fixed your receiver.
Best practices
- Verify signatures on every request; reject stale timestamps.
- Make your handler idempotent: a delivery can arrive more than once. Dedupe on
svix-idor the event id. - Subscribe narrowly with
event_typesto cut noise. - Return
2xxfast; process asynchronously.
Updated about 1 hour ago

