TutorialsReceive webhooks

Receive webhooks

Build an endpoint that receives Orchard webhooks, verifies the signature, and reacts to order.paid — the reliable way to drive fulfillment (don’t rely on the checkout redirect; see checkout).

1. Create the endpoint in the admin

In Settings → Webhooks: add your HTTPS URL, select the events (start with order.paid), and copy the signing secret shown once. Use Test to fire a webhook.ping at your endpoint.

2. Receive, verify, react

The two rules: verify the signature over the raw body, and dedupe on the delivery id. A complete Express handler:

import crypto from 'node:crypto';
import express from 'express';
 
const app = express();
const SECRET = process.env.ORCHARD_WEBHOOK_SECRET;
const seen = new Set(); // use a durable store in production
 
// express.raw — we need the exact bytes to verify the signature.
app.post('/webhooks/orchard', express.raw({ type: 'application/json' }), async (req, res) => {
  const rawBody = req.body.toString('utf8');
 
  if (!verify(req.get('X-Orchard-Signature') ?? '', rawBody, SECRET)) {
    return res.status(400).send('bad signature');
  }
 
  const deliveryId = req.get('X-Orchard-Delivery');
  if (seen.has(deliveryId)) return res.status(200).send('ok'); // already handled
  seen.add(deliveryId);
 
  const event = JSON.parse(rawBody);
  if (event.event_type === 'order.paid') {
    await fulfill(event.data.order_id); // your logic — fetch the order, ship it
  }
 
  res.status(200).send('ok'); // any 2xx acknowledges; non-2xx is retried
});
 
function verify(header, rawBody, secret, tolerance = 300) {
  const p = Object.fromEntries(header.split(',').map((s) => {
    const i = s.indexOf('=');
    return [s.slice(0, i), s.slice(i + 1)];
  }));
  const t = Number.parseInt(p.t ?? '', 10);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > tolerance) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  let got;
  try { got = Buffer.from(p.v1 ?? '', 'hex'); } catch { return false; }
  return got.length === expected.length && crypto.timingSafeEqual(got, expected);
}

3. Fetch the detail

Webhook payloads are thin — order.paid carries just { order_id }. Fetch the full order to act on it:

async function fulfill(orderId) {
  const res = await fetch(`https://api.theorchard.dev/api/v1/orders/lookup?id=${orderId}`, {
    headers: { Authorization: `Bearer ${process.env.ORCHARD_SK}` },
  });
  const order = await res.json();
  // ...create a shipment, sync your ERP, etc.
}

4. Respond fast

Acknowledge within the 15-second budget with any 2xx. If your work is slow, enqueue it and return 200 immediately — Orchard treats a slow or non-2xx response as a failure and retries (1m, 5m, 30m, 2h, 8h; 5 attempts). Because you dedupe on the delivery id, a retry is harmless.

Testing locally

Point a tunnel (e.g. ngrok) at your local server, set that URL as the endpoint, and click Test in the admin to send a webhook.ping. Confirm your handler verifies it and returns 200, then place a test order to see a real order.paid.

Reference