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) as secret.
  • events filters what you receive; an empty list means all events. Set url to null to stop.
  • The URL must be public HTTPS.
  • Send a sample order.shipped any time with POST /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#

EventWhen
order.receivedWe have the order (prepaid: once paid).
order.confirmedThe customer confirmed, or no confirmation was needed.
order.shippedHanded to the courier, with tracking.
order.deliveredDelivered; includes margin and marginReleaseAt.
order.returnedtype: rto or return, with charge / credit.
order.cancelledCancelled; cancellationReason says why.
wallet.margin_releasedA margin moved to Available (amount).
product.updatedA product you imported changed.
product.stock_changedIts stock changed, including to zero.
product.removedIt 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/deliveries or 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.shipped may land after order.delivered). Compare status with what you have, or re-read the order with GET /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.