Help · Webhooks

Connecting your own system with webhooks

Webhooks push order and return events — with full order, customer, and item detail — from Checkout DM to your own CRM, ERP, or automation stack the moment they happen. No polling required.

Webhooks are a Pro plan feature. Upgrade to Pro

Set it up

1

Add your endpoint

Go to Webhooks in your dashboard, add your HTTPS endpoint URL, and pick which events you want. Up to 10 webhooks per account.

2

Save your signing secret

You'll see it once, right after creating the webhook. Store it in your own environment variables — Checkout DM never shows it again.

3

Receive events

From then on, matching events are POSTed to your URL in real time as they happen in your Checkout DM dashboard or over Instagram DM.

Available events

  • order.createdA new order is placed, before payment. No order_number or status yet — those are assigned once paid.
  • order.paidPayment confirmed — via Razorpay, Stripe, PayPal, or manual/UPI.
  • order.shippedYou mark the order as shipped from the dashboard. Includes shipped_at.
  • return.requestedA customer asks for a return, from the dashboard or Instagram DM.
  • return.approvedYou approve a return request. Includes merchant_note if you added one.
  • return.rejectedYou reject a return request. Includes merchant_note if you added one.

Payload shape

event, business_id, entity_id, and timestamp are always present. Everything else — order, customer, address, and item detail — is included where it's relevant and available, but the exact field set varies by event type. Below are two real examples; see "Field notes" further down for what differs and why.

order.paid

json
{
  "event": "order.paid",
  "business_id": 4821,
  "entity_id": 91045,
  "timestamp": "2026-08-24T06:24:06.712Z",
  "order_number": "CDM-000044",
  "customer_name": "Jane Doe",
  "customer_phone": "+919876543210",
  "customer_email": "[email protected]",
  "address": {
    "raw": "221B Baker Street, London, NW1 6XE",
    "line1": "221B Baker Street",
    "line2": "",
    "city": "London",
    "state": "",
    "country": "UK",
    "pincode": "NW1 6XE"
  },
  "total_amount": "1148",
  "currency": "INR",
  "status": "paid",
  "shipping_fee": "150",
  "discount": null,
  "items": [
    { "name": "Blue Washed Jeans for Men", "qty": 2, "price": 499, "size": "M" }
  ]
}

return.approved

json
{
  "event": "return.approved",
  "business_id": 4821,
  "entity_id": 31,
  "timestamp": "2026-08-24T06:36:05.260Z",
  "order_number": "CDM-000045",
  "customer_name": "Jane Doe",
  "customer_phone": "+919876543210",
  "customer_email": "[email protected]",
  "address": { "raw": "...", "line1": "...", "line2": "", "city": "...", "state": "...", "country": "...", "pincode": "..." },
  "total_amount": "1949",
  "currency": "INR",
  "shipping_fee": "150",
  "discount": null,
  "reason": "Wrong size ordered, needs return now",
  "merchant_note": null,
  "items": [
    { "name": "Red T-Shirt", "qty": 2, "price": 1, "size": "S" }
  ]
}
Amounts (total_amount, shipping_fee) are sent as strings, not numbers — this avoids floating-point rounding on money values. Item price is a plain number. Parse amounts explicitly rather than assuming a type.

Field notes

Write your integration defensively — check a field exists before reading it, rather than assuming every field appears on every event.

  • order_numberAbsent on order.created — assigned right after payment, so it isn't ready yet at that point.
  • statusPresent on order.paid only ("paid"). Not included on order.created or order.shipped — infer state from the event name itself.
  • customer_emailMay be absent when a return is requested over Instagram DM, since email isn't always captured in that flow. Always present on order events.
  • shipped_atOnly present on order.shipped.
  • reasonOnly present on the three return.* events — the customer's stated reason at request time.
  • merchant_noteOnly present on return.approved / return.rejected — whatever you typed when deciding, or null if you left it blank.
  • discountAlways present as a key, but its value is null whenever no coupon was applied — not omitted.
  • itemsFor return events, this reflects only the specific items being returned when that data is available, falling back to the full order's items otherwise.

Verifying signatures

Every request carries an X-CheckoutDM-Signature header — an HMAC-SHA256 hash of the raw request body, signed with your webhook secret. Always verify it before trusting a payload, using the raw bytes exactly as received — even a single extra space will produce a different hash.

node.js
const crypto = require('crypto');

function isValidSignature(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  // constant-time compare
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}

// Express example
app.post('/checkoutdm-hook', express.raw({ type: '*/*' }), (req, res) => {
  const signature = req.headers['x-checkoutdm-signature'];
  const secret = process.env.CHECKOUTDM_WEBHOOK_SECRET;

  if (!isValidSignature(req.body, signature, secret)) {
    return res.status(401).send('invalid signature');
  }

  const event = JSON.parse(req.body);
  // event.event, event.business_id, event.entity_id — always present.
  // Everything else depends on event type; see "Field notes" below.
  res.status(200).send('ok');
});

Retries & failed deliveries

If your endpoint doesn't respond with a 2xx status, we retry automatically for a few attempts. After that, the delivery stops retrying on its own — but it isn't lost. Head to Webhooks in your dashboard and you'll see it listed under "Failed deliveries," grouped by which webhook it belongs to, with a one-click Resend once your endpoint is back up. Make sure your endpoint returns a 2xx quickly (ideally under a few seconds) once it's accepted the event, and do any slow processing afterwards on your side.

Questions about a specific delivery? Reach out to support with the event type and approximate time and we can look it up on our side.

🍪We use cookies to run your dashboard and, if you allow it, to improve CheckoutDM. Privacy Policy