Skip to content
Merchant guide
HTTPS · Signed events

Quick start — your first webhook

The complete path from zero to your first signed event arriving at your server: prepare the receiver, register in the dashboard, fire a test event, inspect the delivery — and if anything goes wrong, know the cause at first glance.

Reviewed against implementation
YOUR INTEGRATION, AT A GLANCE
Node.js — complete receiver skeleton (Express)
const express = require('express');
const crypto = require('node:crypto');
// Implement this adapter with a durable queue and a UNIQUE delivery-id key.
const { enqueueOnce } = require('./durable-queue');
const app = express();
const secrets = [
  process.env.YALLABUY_WEBHOOK_SECRET,
  process.env.YALLABUY_WEBHOOK_SECRET_PREV,
].filter(secret => typeof secret === 'string' && secret.length >= 32);
if (!secrets.length) throw new Error('Configure the webhook signing secret');

function verifyYallaBuyWebhook(rawBody, headers) {
  const timestamp = String(headers['webhook-timestamp'] ?? '');
  if (!/^\d{10,11}$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const ids = ['webhook-id', 'webhook-event-id', 'webhook-delivery-id']
    .map(name => String(headers[name] ?? ''));
  if (!ids.every(id => /^[A-Za-z0-9:_-]{1,200}$/.test(id))) return false;
  const signature = String(headers['webhook-signature'] ?? '');
  if (!/^v1=[a-f0-9]{64}$/.test(signature)) return false;
  const message = [timestamp, ...ids, rawBody].join('.');
  return secrets.some(secret => {
    const expected = 'v1=' + crypto.createHmac('sha256', secret)
      .update(message, 'utf8').digest('hex');
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  });
}

app.post('/webhooks/yallabuy',
  express.raw({ type: 'application/json', limit: '64kb' }),
  async (req, res) => {
    const raw = req.body.toString('utf8');
    if (!verifyYallaBuyWebhook(raw, req.headers)) return res.status(401).end();
    let event;
    try { event = JSON.parse(raw); }
    catch { return res.status(400).end(); }
    try {
      // ACK only after durable persistence. Duplicate keys must be a no-op.
      await enqueueOnce({ key: req.headers['webhook-delivery-id'], event });
      return res.status(200).end();
    } catch {
      return res.status(503).end(); // No ACK: allow the sender to retry.
    }
  }
);
// Mount before any JSON body parser. Start the server in your app entry point.
See the implementation
On this page

01Prerequisites

  • An active YallaBuy store with an owner account (or integration management permission).
  • A public HTTPS server — port 443 or 8443 only, with a valid TLS certificate matching the hostname. Internal and local addresses are rejected by the platform itself (see the security requirements page).
  • A receiver that accepts POST with a JSON body: reads the RAW body before any parsing (required for signature verification), and responds with any 2xx within 10 seconds.

02Registering in the dashboard

  1. Open "Settings" then "Webhooks" (path /dashboard/settings/integrations/webhooks) and press "Create endpoint". This is the only registration mechanism — there is no public management API for webhooks.
  2. Fill in the fields: name (1–100 chars, unique within your store), URL (HTTPS only, port 443 or 8443, max 2048 chars, no user:pass@), and subscriptions (1–25 types from the 16-type subscribable catalog).
  3. Save and copy the signing secret immediately — it starts with whsec_ and is shown EXACTLY ONCE. If you lose it, rotate the secret from the same screen.
  4. The store limit is 10 endpoints — pause or delete one if you hit the cap before creating another.

03The test event and inspecting your first delivery

  1. Press "Send test event" on the endpoint — within about a minute a signed ping arrives, using the same headers and the exact same signature scheme as real events.
  2. Inspect the request in your server logs: the six webhook headers and the JSON body. The webhooks screen shows the delivery as DELIVERED with the HTTP status your server returned.
  3. Once the ping arrives and verifies, place a test order in your store — the first real business event arrives at the same speed.

If the delivery failed, open the delivery row in the delivery history: every attempt is listed with its outcome, HTTP status and a sanitized error summary. The outcome (TIMEOUT, NON_2XX, BLOCKED_URL, …) usually names the fix itself — the full table is on the troubleshooting page.

04A complete receiver skeleton

Node.js — complete receiver skeleton (Express)
const express = require('express');
const crypto = require('node:crypto');
// Implement this adapter with a durable queue and a UNIQUE delivery-id key.
const { enqueueOnce } = require('./durable-queue');
const app = express();
const secrets = [
  process.env.YALLABUY_WEBHOOK_SECRET,
  process.env.YALLABUY_WEBHOOK_SECRET_PREV,
].filter(secret => typeof secret === 'string' && secret.length >= 32);
if (!secrets.length) throw new Error('Configure the webhook signing secret');

function verifyYallaBuyWebhook(rawBody, headers) {
  const timestamp = String(headers['webhook-timestamp'] ?? '');
  if (!/^\d{10,11}$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const ids = ['webhook-id', 'webhook-event-id', 'webhook-delivery-id']
    .map(name => String(headers[name] ?? ''));
  if (!ids.every(id => /^[A-Za-z0-9:_-]{1,200}$/.test(id))) return false;
  const signature = String(headers['webhook-signature'] ?? '');
  if (!/^v1=[a-f0-9]{64}$/.test(signature)) return false;
  const message = [timestamp, ...ids, rawBody].join('.');
  return secrets.some(secret => {
    const expected = 'v1=' + crypto.createHmac('sha256', secret)
      .update(message, 'utf8').digest('hex');
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  });
}

app.post('/webhooks/yallabuy',
  express.raw({ type: 'application/json', limit: '64kb' }),
  async (req, res) => {
    const raw = req.body.toString('utf8');
    if (!verifyYallaBuyWebhook(raw, req.headers)) return res.status(401).end();
    let event;
    try { event = JSON.parse(raw); }
    catch { return res.status(400).end(); }
    try {
      // ACK only after durable persistence. Duplicate keys must be a no-op.
      await enqueueOnce({ key: req.headers['webhook-delivery-id'], event });
      return res.status(200).end();
    } catch {
      return res.status(503).end(); // No ACK: allow the sender to retry.
    }
  }
);
// Mount before any JSON body parser. Start the server in your app entry point.