Webhooks

Webhooks let you know when something happens to your orders in real time. Instead of polling the API, DoorPay sends an HTTP POST to your server whenever an event occurs.

Setting Up Webhooks

1

Create an endpoint on your server that accepts POST requests with a JSON body. Example: https://yoursite.com/webhooks/doorpay

2

Configure the URL in your Dashboard → Settings. Set separate URLs for sandbox and production.

3

Return a 2xx status from your endpoint. Any non-2xx response (or timeout) triggers automatic retries.

Webhook Secret

Every webhook is signed with an HMAC-SHA256 secret so you can verify it came from DoorPay. The secret is auto-generated when you first save a webhook URL, and shown only once.

1

Set your webhook URL in Dashboard → Settings and click Save. A webhook secret (starting with whsec_) is auto-generated.

2

Copy the secret immediately from the popup dialog. It is only shown once — DoorPay stores it encrypted and cannot retrieve it later.

3

Store it securely in your server's environment variables (e.g. DOORPAY_WEBHOOK_SECRET). Never hard-code it in source.

Rotating the Secret

If your secret is compromised, go to Dashboard → Settings and click Rotate next to the webhook secret status. The old secret is permanently invalidated — update your server immediately with the new one. Separate secrets are maintained for sandbox and production environments.

Event Types

DoorPay sends these webhook events throughout the order lifecycle:

EventWhen It FiresOrder Status After
ORDER_CREATEDYou create a new order via the APIACCEPTED
PAYMENT_SUCCESSBuyer completes paymentPAID
PAYMENT_FAILEDBuyer's payment attempt failsACCEPTED
DELIVERY_CONFIRMEDYou mark the order as deliveredDELIVERED
DISPUTE_RAISEDBuyer raises a disputeDISPUTE_*
DISPUTE_RESOLVEDYou resolve a disputeDELIVERED
ORDER_COMPLETED24-hour window passes with no dispute. Funds released.COMPLETED
REFUND_PROCESSEDBuyer refunded (dispute not resolved in time)REFUNDED
ORDER_CANCELLEDOrder cancelled (by you or payment link expired)CANCELLED

Payload Format

Every webhook sends a JSON POST body with this structure:

Example: PAYMENT_SUCCESS webhook
{
  "event": "PAYMENT_SUCCESS",
  "timestamp": "2026-03-13T10:30:00Z",
  "environment": "sandbox",
  "data": {
    "order_number": "DP-20260313-A7X9K2",
    "merchant_order_id": "SHOP-456",
    "amount": 2500.00,
    "currency": "INR",
    "status": "PAID",
    "dispute_count": 0,
    "buyer": {
      "email": "rahul@example.com",
      "name": "Rahul Sharma"
    }
  }
}
FieldTypeDescription
eventstringThe event type (see table above)
timestampstringISO-8601 UTC timestamp of when the event occurred
environmentstring"sandbox" or "production"
data.order_numberstringDoorPay order number
data.merchant_order_idstringYour internal order ID (if provided at creation)
data.amountnumberOrder amount in INR
data.currencystringAlways "INR"
data.statusstringCurrent order status after this event
data.dispute_countnumberTotal number of disputes raised on this order
data.buyer.emailstringBuyer's email address
data.buyer.namestringBuyer's full name

Webhook Headers

Every webhook request includes these headers:

HeaderDescription
Content-TypeAlways application/json
X-DoorPay-SignatureHMAC-SHA256 hex digest of timestamp.body, signed with your webhook secret. Used with the timestamp header for replay protection.
X-DoorPay-TimestampUnix epoch seconds when the webhook was sent. Reject webhooks older than 5 minutes to prevent replay attacks.
X-DoorPay-EventThe event type (same as the event field in the body)

Verifying Signatures

Always verify the X-DoorPay-Signature header to ensure the webhook came from DoorPay and wasn't tampered with. The signature is computed as HMAC-SHA256(webhook_secret, timestamp + "." + raw_request_body) where timestamp is the value from the X-DoorPay-Timestamp header. Always reject webhooks where the timestamp is older than 5 minutes to prevent replay attacks.

Node.js verification example
const crypto = require("crypto");

function verifyWebhook(rawBody, signature, timestamp, webhookSecret) {
  // Reject webhooks older than 5 minutes (replay protection)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false;
  }

  const signedPayload = timestamp + "." + rawBody;
  const expected = crypto
    .createHmac("sha256", webhookSecret)
    .update(signedPayload, "utf8")
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(expected, "hex")
  );
}

// In your Express handler:
app.post("/webhooks/doorpay", (req, res) => {
  const signature = req.headers["x-doorpay-signature"];
  const timestamp = req.headers["x-doorpay-timestamp"];
  const rawBody = req.rawBody; // make sure to capture raw body

  if (!verifyWebhook(rawBody, signature, timestamp, process.env.DOORPAY_WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }

  const event = req.body;
  console.log("Received event:", event.event, event.data.order_number);

  // Handle the event based on type
  switch (event.event) {
    case "PAYMENT_SUCCESS":
      // Mark the order as paid in your system
      break;
    case "DISPUTE_RAISED":
      // Alert your support team
      break;
    case "ORDER_COMPLETED":
      // Funds released — finalize the order
      break;
  }

  res.status(200).send("OK");
});
Python verification example
import hmac
import hashlib
import time

def verify_webhook(raw_body: bytes, signature: str, timestamp: str, webhook_secret: str) -> bool:
    # Reject webhooks older than 5 minutes (replay protection)
    if abs(time.time() - int(timestamp)) > 300:
        return False

    signed_payload = f"{timestamp}.".encode("utf-8") + raw_body
    expected = hmac.new(
        webhook_secret.encode("utf-8"),
        signed_payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

# In your Flask handler:
@app.route("/webhooks/doorpay", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-DoorPay-Signature")
    timestamp = request.headers.get("X-DoorPay-Timestamp")
    if not verify_webhook(request.data, signature, timestamp, WEBHOOK_SECRET):
        return "Invalid signature", 401

    event = request.json
    print(f"Received: {event['event']} for {event['data']['order_number']}")

    return "OK", 200

Retry Policy

If your endpoint returns a non-2xx status code or doesn't respond, DoorPay retries with exponential backoff:

AttemptDelay
1stImmediate
2nd5 minutes
3rd30 minutes
4th2 hours
5th12 hours

After 5 failed attempts, the webhook is marked as FAILED. You can manually retry failed webhooks from the Dashboard → Webhooks page.

Best Practices

Return 200 quickly

Process the webhook asynchronously. Return 200 immediately and handle the event in a background job. If your endpoint takes too long, the request may time out and trigger a retry.

Handle duplicates

Due to retries, you may receive the same event more than once. Use the order_number + event type to deduplicate. Make your handlers idempotent.

Always verify signatures

Never trust a webhook without verifying the X-DoorPay-Signature header. This prevents attackers from sending fake events to your endpoint.

Use HTTPS

Production webhook URLs must use HTTPS. Sandbox allows HTTP for local testing. URLs cannot point to localhost, private IPs, or cloud metadata endpoints.

Reject stale webhooks

Check the X-DoorPay-Timestamp header. Reject any webhook where the timestamp is more than 5 minutes old — this prevents replay attacks.

Don't rely solely on webhooks

Use webhooks for real-time updates, but also poll GET /orders/{id} as a fallback. Network issues can delay or drop webhooks.