Guides
Webhooks
We POST an event to your server whenever an order or a product you listed changes — no polling needed.
Set your endpoint#
curl -X PUT "https://backend.markitmall.com/dropship/v1/webhooks" \
-H "Authorization: Bearer mk_test_XXXX:sk_test_XXXX" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/markitmall/webhook",
"events": [
"order.shipped",
"order.delivered",
"order.returned",
"order.cancelled"
]
}'- The first time you set a URL, the response includes
secret— shown once. Store it next to your API key. Rotate it with"rotateSecret": true, or send your own (32+ characters) assecret. eventsfilters what you receive; an empty list means all events. Seturltonullto stop.- The URL must be public HTTPS.
- Send a sample
order.shippedany time withPOST /webhooks/test.
What we send#
POST https://example.com/markitmall/webhook
Content-Type: application/json
X-MarkitMall-Event: order.shipped
X-MarkitMall-Event-Id: evt_6702f1c4a9b8e3d2f1a0b9c8
X-MarkitMall-Timestamp: 1791281200
X-MarkitMall-Signature: sha256=5f8a2c…
{
"id": "evt_6702f1c4a9b8e3d2f1a0b9c8",
"event": "order.shipped",
"mode": "live",
"createdAt": "2026-10-06T10:00:00.000Z",
"data": {
"id": "6702e9b1c4a9b8e3d2f1a0b7",
"externalOrderId": "1042",
"externalOrderName": "#1042",
"parcel": { "part": 1, "of": 1 },
"orderNumber": "DS-mfx2k9qz-A7C3Q",
"status": "SHIPPED",
"paymentMode": "COD",
"codAmount": 1700,
"tracking": { "number": "LHR123456789", "courier": "MarkitMall Logistics", "url": null }
}
}data is the order (or product) as it is now — the same shape as GET /orders/{id}. Field lists for each event are in the webhook events reference.
Events#
| Event | When |
|---|---|
order.received | We have the order (prepaid: once paid). |
order.confirmed | The customer confirmed, or no confirmation was needed. |
order.shipped | Handed to the courier, with tracking. |
order.delivered | Delivered; includes margin and marginReleaseAt. |
order.returned | type: rto or return, with charge / credit. |
order.cancelled | Cancelled; cancellationReason says why. |
wallet.margin_released | A margin moved to Available (amount). |
product.updated | A product you imported changed. |
product.stock_changed | Its stock changed, including to zero. |
product.removed | It is no longer available for dropshipping. |
Verify the signature#
X-MarkitMall-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your webhook secret. Reject anything that does not match — anyone can POST to your URL.
import crypto from "crypto";
import express from "express";
const app = express();
// Verify against the RAW body — re-serialised JSON will not match.
app.post("/markitmall/webhook", express.raw({ type: "application/json" }), async (req, res) => {
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.MARKITMALL_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const given = req.get("X-MarkitMall-Signature") || "";
if (expected.length !== given.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString("utf8"));
if (await alreadyProcessed(event.id)) return res.status(200).end(); // retries reuse the id
res.status(200).end(); // answer fast…
await handle(event); // …then do the work (better: put it on a queue)
});Delivery and retries#
- Answer with any 2xx within 10 seconds. Do slow work after responding.
- Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then marked failed. See every attempt with
GET /webhooks/deliveriesor in your dashboard. - A retry carries the same
X-MarkitMall-Event-Id— use it to process each event once. - Events can arrive out of order (a retry of
order.shippedmay land afterorder.delivered). Comparestatuswith what you have, or re-read the order withGET /orders/{id}when in doubt.
Webhooks are the fast path, not the only one. A nightly job that re-reads your open orders (
GET /orders?status=CONFIRMED,PROCESSING,SHIPPED) catches anything your server missed while it was down.