Developers
Webhooks
Get a signed HTTPS request on your server whenever orders, customers, products, stock or reviews change in your store.
A webhook is an endpoint on your server that eCommFly sends store events to. When something you subscribe to happens, such as a new order, eCommFly sends a POST request to your endpoint with the record as JSON. Each request is signed with a secret, so you can check it came from your store.
Webhooks are included on the Growth and Scale plans. Only the store owner can add and manage them.
Add an endpoint
- In the dashboard, open Settings → Webhooks and select Add Webhook.
- Give it a Name you’ll recognise, such as
ERP order sync. - Enter the Endpoint URL. It must start with
https://and point to a public website. Private and local network addresses are refused. - Choose the Events to send to it, then save.
- Open the webhook and copy its Signing secret. Use Reveal to see it, or Copy to copy it. Store it on your server, for example in an environment variable.
- Select Send Test to post a
webhook.testevent straight away and see what your endpoint answered.
A store can have up to 10 webhooks, each with its own URL, events and secret.
Events
| Event | Sent when |
|---|---|
order.created |
An order is placed, from the storefront, a landing page or the dashboard. |
order.updated |
Anything on an order changes: its status, payment, shipment, items, address or confirmation. One save can send it more than once. |
order.paid |
An order’s payment status becomes paid. |
order.shipped |
An order’s shipment status becomes shipped. |
order.delivered |
An order’s shipment status becomes delivered. |
order.cancelled |
An order is cancelled. |
order.refunded |
An order’s payment status becomes refunded. |
customer.created |
A customer signs up, including with Google, or is added from the dashboard. |
product.created |
A product is created, duplicated or imported. |
product.updated |
A product is saved, or its status changes. |
product.deleted |
A product is deleted. |
inventory.low_stock |
A variation’s stock falls to its low-stock threshold. |
review.created |
A customer submits a review, or you add one. |
webhook.test |
You select Send Test. You can’t subscribe to it. |
The specific order events come with an order.updated as well. When an order is paid, for example, an endpoint subscribed to both gets order.updated and order.paid.
The request
Every event is a POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
Ecommfly-Webhooks/1.0 |
X-Ecommfly-Event |
The event, such as order.created. |
X-Ecommfly-Delivery |
The ID of this delivery. Each resend gets a new one. |
X-Ecommfly-Signature |
t={unix timestamp},v1={signature}. See Verify the signature. |
The body is an envelope around the record:
| Field | Description |
|---|---|
id |
The event’s ID. It stays the same when a delivery is retried or resent, so use it to skip events you’ve already handled. |
event |
The event, such as order.created. |
createdAt |
When the event happened, in ISO 8601 UTC. |
storeId |
The store’s ID. |
data |
The record, as it was saved. |
What’s in data depends on the event:
| Events | data |
|---|---|
order.* |
The order, with its lineItems, shippingAddress, billingAddress, transactions and shipments. |
customer.created |
The customer. |
product.created, product.updated |
The product, with its variations and media. |
product.deleted |
Only the product’s id, title and slug. |
inventory.low_stock |
The variation, with its stockQuantity and lowStockThreshold, and a product with its id, title and slug. |
review.created |
The review, with its product. |
webhook.test |
A message saying the test came from your store. |
Example: order.created
Shortened to the most used fields. Amounts are strings with two decimal places, in the store’s currency.
{
"id": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
"event": "order.created",
"createdAt": "2026-10-27T10:00:00.000000Z",
"storeId": 12,
"data": {
"id": 4821,
"orderNo": "1052",
"customerId": 977,
"name": "Nusrat Jahan",
"email": null,
"phone": "01712345678",
"status": "pending",
"statusLabel": "Pending",
"paymentStatus": 0,
"paymentStatusLabel": "Unpaid",
"shipmentStatus": 0,
"shipmentStatusLabel": "Pending",
"paymentMethod": "cod",
"currency": "BDT",
"subtotal": "1730.00",
"discountAmount": "0.00",
"shippingCharge": "120.00",
"total": "1850.00",
"paidAmount": "0.00",
"note": null,
"lineItems": [
{
"id": 9310,
"productId": 311,
"variationId": 702,
"title": "Cotton Kurti",
"sku": "KRT-RED-M",
"price": "1730.00",
"quantity": 1,
"total": "1730.00"
}
],
"shippingAddress": {
"phone": "01712345678",
"address1": "House 12, Road 5, Mirpur 10",
"city": "Dhaka"
},
"transactions": [],
"shipments": [],
"createdAt": "2026-10-27T10:00:00.000000Z",
"updatedAt": "2026-10-27T10:00:00.000000Z"
}
}
An order’s status is one of pending, confirmed, no_response, completed, refunded or cancelled. paymentStatus and shipmentStatus are numbers, each with a …Label beside it:
paymentStatus |
Label |
|---|---|
0 |
Unpaid |
1 |
Paid |
2 |
Partially Paid |
3 |
Refunded |
4 |
Failed |
shipmentStatus |
Label |
|---|---|
0 |
Pending |
1 |
Processing |
5 |
Packed |
6 |
Handed Over to Courier |
2 |
Shipped |
3 |
Delivered |
4 |
Cancelled |
Example: webhook.test
{
"id": "0c8e7b4a-2f1d-4e3c-9b5a-6d7e8f9a0b1c",
"event": "webhook.test",
"createdAt": "2026-10-27T10:00:00.000000Z",
"storeId": 12,
"data": {
"message": "This is a test webhook from Deshi Bazar."
}
}
Verify the signature
Check every request before you act on it. The X-Ecommfly-Signature header looks like this:
t=1793095200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
t is when the request was sent, as a Unix timestamp. v1 is the hex HMAC-SHA256 of {t}.{raw body}, keyed with your webhook’s signing secret. To verify it:
- Read the raw request body exactly as it arrived. Don’t parse the JSON and encode it again, since that can change the bytes.
- Compute the HMAC-SHA256 of
t, a full stop and the raw body, with your signing secret as the key. - Compare it with
v1using a constant-time comparison. - Reject the request if
tis more than five minutes from your server’s clock, so an old request can’t be replayed.
If any check fails, answer 400 and ignore the request.
Node.js
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const secret = process.env.ECOMMFLY_WEBHOOK_SECRET;
// express.raw keeps the body as the exact bytes that were signed.
app.post('/webhooks/ecommfly', express.raw({ type: 'application/json' }), (req, res) => {
const { t, v1 } = Object.fromEntries((req.get('X-Ecommfly-Signature') ?? '').split(',').map((part) => part.split('=')));
const expected = crypto.createHmac('sha256', secret).update(`${t}.${req.body}`).digest('hex');
const valid =
typeof v1 === 'string' &&
v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) &&
Math.abs(Date.now() / 1000 - Number(t)) < 300;
if (!valid) return res.sendStatus(400);
const event = JSON.parse(req.body);
// Queue the work, then answer straight away.
res.sendStatus(200);
});
PHP
$body = file_get_contents('php://input');
[$t, $v1] = sscanf($_SERVER['HTTP_X_ECOMMFLY_SIGNATURE'] ?? '', 't=%d,v1=%s');
$expected = hash_hmac('sha256', "{$t}.{$body}", getenv('ECOMMFLY_WEBHOOK_SECRET'));
if (! is_string($v1) || ! hash_equals($expected, $v1) || abs(time() - $t) > 300) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// Queue the work, then answer straight away.
http_response_code(200);
Python
import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["ECOMMFLY_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/ecommfly")
def ecommfly_webhook():
header = request.headers.get("X-Ecommfly-Signature", "")
parts = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
t, v1 = parts.get("t", ""), parts.get("v1", "")
expected = hmac.new(SECRET, f"{t}.".encode() + request.get_data(), hashlib.sha256).hexdigest()
if not t.isdigit() or not hmac.compare_digest(expected, v1) or abs(time.time() - int(t)) > 300:
abort(400)
event = request.get_json()
# Queue the work, then answer straight away.
return "", 200
Respond and retries
Answer with any 2xx status within 10 seconds and the delivery counts as done. Do the real work, such as calling other services, after you’ve answered, for example from a queue.
Anything else counts as a failure: another status, a timeout, a connection error, or a redirect, since redirects aren’t followed. A failed delivery is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. If the last of these six attempts also fails, the delivery is marked failed.
If 20 deliveries in a row fail after every retry, eCommFly turns the webhook off and emails the store owner. A successful delivery or test resets the count. To start again, fix your endpoint, select Send Test to check it, then edit the webhook and turn Active back on.
Handle repeats and order
- The same event can arrive twice, for example when your server handled a request but its answer didn’t reach us. Save each event’s
idand skip ones you’ve already handled. - Events can arrive out of order, because each delivery is retried on its own schedule. Use the record’s
updatedAtto keep the newest version, or fetch the latest state before acting.
Deliveries
Each webhook’s page lists its Deliveries, newest first: the event, its status (Pending, Succeeded or Failed), the number of attempts and the time of the last one. Open a delivery to see the request body that was sent and your endpoint’s answer, or the reason it failed.
Select Resend on a delivery to send the same body to the endpoint again, as a new delivery that’s retried like any other. It keeps the event’s id. Delivery history is kept for 60 days.
Checklist
- Serve your endpoint over HTTPS on a public address, with no redirects.
- Verify the signature and the timestamp on every request, using the raw body.
- Answer
2xxwithin 10 seconds and do slow work afterwards. - Skip events whose
idyou’ve already handled. - Keep your signing secret on the server only. Never put it in a mobile app or a web page.
