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.