Skip to content

Outbound webhooks

Verified

A webhook is the opposite of REST and MCP: you do not ask, MitoOps calls you. When an event you subscribe to occurs in your account, we POST a short signed envelope to your address. The envelope carries identifiers and statuses, not the content of the order — your system reads the detail over the REST interface with its own key.

Subscriptions are managed in the application: Settings → API & MCP → Outbound webhooks. They are available from the Start plan; the Outbound webhooks permission (read or manage) is required.

An endpoint has a name, an address, a list of events and a store scope. On creation you receive a secret — shown once, never again; you can rotate it at any time and the old one stops working immediately.

  • The address must be public and use https:// on the standard port. Internal-network addresses (the local machine, private ranges, cloud metadata, VPN ranges) are rejected on save and on every delivery — the name is resolved again and the connection is pinned to the verified addresses.
  • Redirects are not followed. A 3xx response is a failure.
  • Stores: all (including ones added later) or selected. An event from a store outside the scope is not sent.
  • A test delivery from the button sends a ping event along the same path as a real one — including the signature and the log entry.
  • An endpoint can be disabled (pending deliveries are skipped) and deleted (the log remains).
Event When resource.type Fields in data
order.created new order from the store order order_code
order.paid order marked as paid order order_code
order.status_changed order status changed order order_code, status_id, previous_status_id
shipment.created shipment created with the carrier shipment shipment_id, order_code, carrier_code, tracking_number
shipment.status_changed shipment status changed (carrier or staff) shipment shipment_id, order_code, carrier_code, status, previous_status, tracking_number
shipment.delivered shipment delivered; in addition to shipment.status_changed shipment as above, status is DELIVERED
inventory.stock_changed stock movement of an item inventory_item item_id, sku, kind
invoice.created invoice issued invoice invoice_code, order_code
claim.created claim opened (by the customer or by staff) claim claim_code, order_code
claim.status_changed claim status changed claim claim_code, order_code, status_id
claim.closed claim closed claim claim_code, order_code
ping test delivery from the button webhook_endpoint message, endpoint

Events come from the same business layer as automations: whatever triggers an automation can also be subscribed to as a webhook. Return shipments are not sent as shipment.* — they belong to the claim.

Every delivery is JSON with an envelope version. A new field may be added without changing the version; a change in a field’s meaning is a new version.

{
"event_id": "9b2f0c1e-6d3a-4c7e-9a2b-0f1e2d3c4b5a",
"event_type": "shipment.delivered",
"version": 1,
"occurred_at": "2026-09-03T08:41:12.318Z",
"tenant": "00001",
"store": "sk",
"resource": { "type": "shipment", "id": "5521" },
"data": {
"shipment_id": "5521",
"order_code": "2026001234",
"carrier_code": "gls",
"status": "DELIVERED",
"previous_status": "IN_TRANSIT",
"tracking_number": "GLS123456789"
}
}
  • event_id is unique per event. If two of your endpoints subscribe to the same event, they receive the same ID. A retry carries the same ID and the same body.
  • store is the store code (market) — the same one REST uses.
  • tenant is the identifier of your MitoOps account.
Content-Type: application/json
User-Agent: MitoOps-Webhooks/1
X-MitoOps-Event: shipment.delivered
X-MitoOps-Event-Id: 9b2f0c1e-6d3a-4c7e-9a2b-0f1e2d3c4b5a
X-MitoOps-Timestamp: 1756888872
X-MitoOps-Signature: v1=3f1a…c9e0
X-MitoOps-Delivery: 184

The signature is HMAC-SHA256 keyed with the endpoint secret over the string:

<X-MitoOps-Timestamp> + "." + <exact request body>

The result is hexadecimal and appears in the header with the version prefix v1=. The timestamp is Unix time in seconds.

Verify over the raw request body (the bytes as received), not over re-serialised JSON — any change in whitespace or key order invalidates the signature. Compare in constant time and reject messages older than 5 minutes.

import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyMitoOpsWebhook({ secret, headers, rawBody, now = Math.floor(Date.now() / 1000) }) {
const ts = Number(headers['x-mitoops-timestamp']);
if (!Number.isFinite(ts) || Math.abs(now - ts) > 300) return false; // old or missing timestamp
const received = String(headers['x-mitoops-signature'] || '')
.split(',').map((s) => s.trim()).find((s) => s.startsWith('v1='));
if (!received) return false;
const expected = createHmac('sha256', secret)
.update(String(ts) + '.').update(rawBody).digest('hex');
const a = Buffer.from(received.slice(3), 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}
import hmac, hashlib, time
def verify_mitoops_webhook(secret: str, headers: dict, raw_body: bytes) -> bool:
try:
ts = int(headers["X-MitoOps-Timestamp"])
except (KeyError, ValueError):
return False
if abs(int(time.time()) - ts) > 300:
return False
sig = next((s.strip() for s in headers.get("X-MitoOps-Signature", "").split(",") if s.strip().startswith("v1=")), None)
if not sig:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(sig[3:], expected)

The signature covers the timestamp, so a captured message cannot be re-sent later with a fresh time. Alongside the time check, keep the event_id of recent deliveries and ignore duplicates: a repeated delivery is a property of the system, not an error.

  • Respond with 2xx within 10 seconds. The response body is not read. Queue any longer processing in the background and respond immediately.
  • Any other response (4xx, 5xx), a timeout or a connection error is a failure and is retried with back-offs of 1, 5, 15, 60 and 240 minutes; six attempts in total. After that the delivery is FAILED and waits for a manual retry from the log.
  • A 3xx response and an address that no longer resolves to a public network are failures without retry — they do not fix themselves.
  • The delivery log in the application shows the status, HTTP code, time, attempt count and event for every attempt. The message body is not shown in the log.
Status Meaning
PENDING waiting for an attempt or for the next retry
SENT the receiver responded 2xx
FAILED attempts exhausted or a permanent error; can be retried manually
SKIPPED the endpoint was disabled or deleted in the meantime
  • The secret is stored encrypted and is never written to logs or to the delivery log. It is shown once; rotation works even when the plan no longer includes webhooks — it is a security operation.
  • No personal data. The envelope carries identifiers and statuses. The customer’s name, address and e-mail are not sent; whoever needs them reads them over REST with their own scope and audit trail.
  • HTTPS only, public addresses only, no redirects. The check on save and on every delivery protects against routing into an internal network and against DNS changes.
  • A subscription is created by a person, not by a key. Neither a REST access key nor an OAuth consent can create a subscription: reading on request and a permanent outbound stream of events are two different permissions.
  • Isolation. Subscriptions and the log live in your account’s space; the store scope is evaluated on every event.
Plan Outbound webhooks Endpoints
Trial 0
Beginner 0
Start yes 5
Growth yes 20
Pro yes 50
Custom by agreement by agreement

Non-deleted endpoints count, enabled or disabled. Downgrading does not delete existing endpoints; no new one can be added until the count drops below the limit, and on a plan without webhooks events stop being sent until the plan is restored.

An endpoint can also be the target of the Send webhook step in Automations: the message is sent only once the rule’s conditions are met, not on every event. The step only picks the endpoint — the address, secret and store scope stay in the registry.

The message has type automation.action (it cannot be subscribed to directly) and the same envelope, signature, retries and log as other events. In data it carries automation.workflow_id, automation.run_id, automation.node_id, automation.source_event (the event that woke the rule) and the identifiers of the order, shipment or claim; resource is the run’s entity. Load details through the REST API.

If the receiver fails 30 attempts in a row or an endpoint has more than 5,000 pending deliveries, MitoOps pauses the endpoint and notifies you. Pending deliveries stay; fix the receiver, enable the endpoint and they resume. The delivery log is kept for 30 days.