Webhooks
Ecomone sends an HTTPS POST to your endpoint when something happens in a shop, so you don't have to poll.
Set up a webhook#
The merchant adds webhooks in the backend under Settings › Automations: a name, your https URL, the events and an on/off switch. A shop can have up to 10 webhooks. Each webhook has its own signing secret, which the merchant copies to you (it can be rotated). The backend can also send a test delivery ("test": true).
Events#
| Event | When it is sent | Payload |
|---|---|---|
order.paid | An order was paid successfully. | order |
order.refunded | Once per successful refund: refunds made in the Ecomone backend or through MCP, refunds made by a cancellation (including PATCH /orders/{id} with action: "cancel"), and refunds made directly in Stripe. Every partial refund is a separate event. The payload has an extra refund object. | order + refund |
order.cancelled | An order was cancelled in the Ecomone backend, through MCP or with PATCH /orders/{id} (action: "cancel"). Not sent if the refund of the cancellation fails. | order |
order.shipped | The order’s fulfillment status changed to shipped — through the API, the Ecomone backend or MCP. A later re-shipment (new tracking number) is delivered as a new event. | order |
order.fulfilled | The order’s fulfillment status changed to fulfilled — through the API, the Ecomone backend or MCP. A later re-shipment (new tracking number) is delivered as a new event. | order |
digital.delivered | Sent right after order.paid when the order grants access to digital content. | order |
newsletter.subscribed | Someone subscribed to the shop’s newsletter. | subscriber |
Request format#
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Ecomone-Webhooks/1 |
X-Ecomone-Event | <event> |
X-Ecomone-Event-Id | <event_id> |
X-Ecomone-Signature | <hex HMAC-SHA256 of the raw body> |
{
"event": "order.paid",
"event_id": "order.paid:9f1c2b7e-4a55-4d1f-9b62-1c0a3f6d8e21",
"created_at": "2026-08-28T09:14:25.000Z",
"data": {
"order": {
"id": "9f1c2b7e-4a55-4d1f-9b62-1c0a3f6d8e21",
"number": 1042,
"status": "paid",
"fulfillment_status": "unfulfilled",
"tracking_number": null,
"tracking_carrier": null,
"tracking_url": null,
"shipped_at": null,
"created_at": "2026-08-28T09:13:58.000Z",
"paid_at": "2026-08-28T09:14:22.000Z",
"refunded_at": null,
"cancelled_at": null,
"amount_cents": 4900,
"shipping_cents": 0,
"currency": "CHF",
"is_direct_sale": true,
"direct_offer": {
"id": "11111111-1111-4111-8111-111111111111",
"title": "Online course summer offer",
"slug": "summer-offer"
}
},
"customer": {
"email": "jane.doe@example.com",
"first_name": "Jane",
"last_name": "Doe",
"name": "Jane Doe",
"address": {
"line1": "Example Street 1",
"line2": null,
"postal_code": "8000",
"city": "Zurich",
"state": null,
"country": "CH",
"company": null,
"phone": "+41 79 000 00 00"
},
"company": null,
"vat_id": null,
"vat_id_status": null
},
"items": [
{
"product_id": "22222222-2222-4222-8222-222222222222",
"title": "Online course: First steps",
"variant": null,
"sku": "COURSE-001",
"quantity": 1,
"unit_price_cents": 4900,
"line_total_cents": 4900,
"personalization": [],
"personalization_surcharge_cents": 0
}
]
}
}Payloads#
Every delivery has the same envelope: event, event_id (stable across retries — use it to de-duplicate), created_at, data and test. Order events contain data.order, data.customer and data.items. Payment details and download links are never included.
{
"subscriber": {
"email": "string (lower-case)",
"first_name": "string|null",
"source": "string|null",
"subscribed_at": "timestamp"
}
}Verify the signature#
X-Ecomone-Signature is the HMAC-SHA256, hex-encoded, over the exact raw request body (UTF-8). Compute the HMAC with your webhook secret over the raw body before parsing JSON, and compare in constant time.
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/ecomone/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const expected = crypto.createHmac("sha256", process.env.ECOMONE_WEBHOOK_SECRET)
.update(req.body) // raw Buffer
.digest("hex");
const given = req.get("X-Ecomone-Signature") ?? "";
const ok = given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
// de-duplicate on event.event_id, then process asynchronously
res.status(200).end();
});import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/ecomone/webhooks")
def webhook():
raw = request.get_data() # raw bytes
expected = hmac.new(os.environ["ECOMONE_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Ecomone-Signature", "")):
abort(401)
event = request.get_json()
# de-duplicate on event["event_id"]
return "", 200<?php
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, getenv('ECOMONE_WEBHOOK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_ECOMONE_SIGNATURE'] ?? '')) {
http_response_code(401); exit;
}
$event = json_decode($raw, true);
http_response_code(200);The signature does not include a timestamp. Protect against replays by storing processed event_ids.
Delivery & retries#
- Any
2xxresponse counts as delivered. Respond within 10 seconds. - 5 retries after 1, 5, 30, 120 and 360 minutes (6 attempts total), processed by a once-per-minute job.
- After 20 consecutive deliveries that exhausted all attempts, the webhook is deactivated and the shop owner gets an in-app notification. Re-activating resets the counter.
- Order of deliveries is not guaranteed. Each event is delivered at most once per webhook and
event_id. - Delivery logs are kept for 30 days and visible to the merchant.
Best practices#
- Acknowledge fast with
200, then process in a queue. - Fetch the latest state from the REST API (for example
GET /orders/{id}) if you need more fields. - Make handlers idempotent: retries reuse the same
event_id.