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.
Set it up
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.
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.
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
{
"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
{
"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" }
]
}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.
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.
