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"]
  }'
FieldRequiredNotes
nameyesA label for the endpoint
urlyesPublic HTTPS receiver. Internal and private addresses are rejected
event_typesnoThe event types to receive. A webhook with none receives nothing
descriptionnoFree 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 typeSent when
po.createdA 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_changedA purchase order's status changes
shipment.createdA shipment is created: in the app, through the API or by an import
shipment.status_changedA shipment's status changes
shipment.tracking_updatedA shipment's tracking details change
pod.submittedA proof of delivery is submitted
supplier.status_changedA 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:

HeaderMeaning
svix-idUnique delivery id
svix-timestampUnix seconds when the signature was generated
svix-signatureSpace-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-signature

Respond 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:

FieldMeaning
attemptsHow many times it has been tried
last_statusThe last HTTP status your endpoint returned
last_errorThe last transport or HTTP error
next_attempt_atWhen the next retry is scheduled
delivered_atWhen it finally succeeded
dead_lettered_atWhen 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-id or the event id.
  • Subscribe narrowly with event_types to cut noise.
  • Return 2xx fast; process asynchronously.

Did this page help you?