Skip to content

The eCommFly app is out on Android.

Get it on Google PlayiOS coming soon

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

  1. In the dashboard, open Settings → Webhooks and select Add Webhook.
  2. Give it a Name you’ll recognise, such as ERP order sync.
  3. Enter the Endpoint URL. It must start with https:// and point to a public website. Private and local network addresses are refused.
  4. Choose the Events to send to it, then save.
  5. 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.
  6. Select Send Test to post a webhook.test event 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:

  1. Read the raw request body exactly as it arrived. Don’t parse the JSON and encode it again, since that can change the bytes.
  2. Compute the HMAC-SHA256 of t, a full stop and the raw body, with your signing secret as the key.
  3. Compare it with v1 using a constant-time comparison.
  4. Reject the request if t is 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 id and 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 updatedAt to 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 2xx within 10 seconds and do slow work afterwards.
  • Skip events whose id you’ve already handled.
  • Keep your signing secret on the server only. Never put it in a mobile app or a web page.