Skip to main content
This guide walks through the complete process of setting up an RMZ webhook, building a receiver endpoint, verifying signatures, and handling common scenarios.

Prerequisites

  • An RMZ store with an RMZ+ subscription plan
  • A publicly accessible HTTPS endpoint to receive webhooks
  • Basic knowledge of your server framework (Express, Flask, Laravel, etc.)

Step 1: Create a Webhook in the Dashboard

1

Navigate to webhook settings

Go to Dashboard > Settings > Webhooks.
2

Add a new webhook

Click Add Webhook and fill in the configuration:
FieldExample Value
NameOrder Notifications
Eventorder.created
URLhttps://api.yourdomain.com/webhooks/rmz
Tries3
3

Save and note your secret key

After saving, the webhook’s secret key (28 characters) is shown. Copy and store it securely — you will need it to verify signatures.
4

Enable the webhook

Toggle the webhook to Enabled when you are ready to receive events.

Step 2: Build Your Receiver Endpoint

Your endpoint must accept HTTP POST requests, verify the signature, process the payload, and return a 200 status quickly.
const express = require("express");
const crypto = require("crypto");

const app = express();

// IMPORTANT: Use raw body for signature verification
app.post(
  "/webhooks/rmz",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const signature = req.headers["signature"];
    const requestId = req.headers["x-rmz-request-id"];
    const rawBody = req.body.toString();

    // 1. Verify signature
    const expectedSig = crypto
      .createHmac("sha256", process.env.RMZ_WEBHOOK_SECRET)
      .update(rawBody)
      .digest("hex");

    if (!crypto.timingSafeEqual(
      Buffer.from(signature || ""),
      Buffer.from(expectedSig)
    )) {
      console.error("Invalid webhook signature");
      return res.status(401).json({ error: "Invalid signature" });
    }

    // 2. Parse payload
    const payload = JSON.parse(rawBody);
    console.log(`Received ${payload.event} (request: ${requestId})`);

    // 3. Respond immediately
    res.status(200).json({ received: true });

    // 4. Process asynchronously
    processWebhook(payload, requestId);
  }
);

async function processWebhook(payload, requestId) {
  switch (payload.event) {
    case "order.created":
      const order = payload.data;
      console.log(`New order #${order.id} from ${order.customer.firstName}`);
      console.log(`Amount: ${order.total}, Method: ${order.transaction.payment_method}`);
      // Your business logic here...
      break;

    case "order.status.changed":
      const updated = payload.data;
      const currentStatus = updated.status.status;
      console.log(`Order #${updated.id} status changed to ${currentStatus}`);
      break;
  }
}

app.listen(3000);
If you are using Laravel, make sure to exclude the webhook route from CSRF verification by adding it to the $except array in App\Http\Middleware\VerifyCsrfToken.

Step 3: Handle Retries and Idempotency

Webhooks may be delivered more than once. Use the X-RMZ-REQUEST-ID header to detect and ignore duplicates:
// In-memory for demo — use Redis or a database in production
const processedIds = new Set();

function isProcessed(requestId) {
  if (processedIds.has(requestId)) {
    return true;
  }
  processedIds.add(requestId);
  return false;
}

// In your webhook handler:
if (isProcessed(requestId)) {
  console.log(`Duplicate webhook ${requestId}, skipping`);
  return res.status(200).json({ received: true });
}
Store processed request IDs in Redis with a 7-day TTL for automatic cleanup. In a database, use a unique constraint on the request ID column and catch the duplicate key error.

Step 4: Test Your Webhook

Before enabling the webhook for production:
  1. Use a tunneling tool like ngrok to expose your local server:
    ngrok http 3000
    
    Use the generated HTTPS URL as your webhook URL.
  2. Place a test order in your store to trigger the order.created event.
  3. Check the webhook logs in your dashboard to see the delivery status and response from your server.
  4. Verify the payload matches the expected format documented in Payload Format.

Common Patterns

Sending Slack Notifications

async function notifySlack(order) {
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      text: `New order #${order.id}`,
      blocks: [
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: `*New Order #${order.id}*\n` +
              `Customer: ${order.customer.firstName} ${order.customer.lastName}\n` +
              `Amount: ${order.total} SAR\n` +
              `Payment: ${order.transaction.payment_method}`
          }
        }
      ]
    })
  });
}

Syncing to a Database

async function syncOrder(order) {
  await db.query(
    `INSERT INTO orders (rmz_order_id, customer_email, amount, status, created_at)
     VALUES ($1, $2, $3, $4, $5)
     ON CONFLICT (rmz_order_id) DO UPDATE SET status = $4`,
    [order.id, order.customer.email, order.total, order.status.status, order.created_at]
  );
}

Troubleshooting

  • Verify the webhook is enabled in the dashboard
  • Check that your URL is publicly accessible (not localhost)
  • Ensure your endpoint accepts POST requests and returns a 2xx status
  • Check the webhook logs in the dashboard for error details
  • Use the raw request body for verification, not the parsed JSON
  • Ensure you are using the correct secret key for this specific webhook
  • Check for middleware that modifies the request body before your handler
  • This is expected behavior during retries. Implement idempotency using the X-RMZ-REQUEST-ID header.
  • Use the created_at timestamp in the statuses array to determine the correct chronological order.