Skip to main content

Webhooks

Webhooks let Reeds tell you when something happens, so you don't have to poll. Each time a delivery changes status, a wallet is topped up or an invoice is issued, Reeds sends a signed POST to your server.

Set up an endpoint​

  1. Open Dashboard → Developers and pick the mode (Test or Live) you are working in.
  2. Enter your endpoint URL. Test and Live each have their own URL.
  3. Copy the signing secret: whsec_test_... for a Test endpoint, whsec_live_... for a Live one. Each endpoint has its own secret, separate from your client credentials, so rotating a client secret never breaks your webhooks.

Your URL must follow these rules:

  • It must use HTTPS, with a host name (not an IP address, localhost or an internal name such as *.local).
  • Reeds refuses a URL that resolves to a private, loopback or link-local address. This is checked when you save the URL and again on every send, and each send goes to the address Reeds checked.
  • Reeds never follows redirects. If your URL answers with a 301 or 302, the attempt counts as failed. Give Reeds the final URL.

To receive Test-mode events while you develop, run a tunnelling tool that gives your local server a public HTTPS host name. Save that URL as your Test endpoint, then send POST /webhooks/test.

What Reeds sends​

Reeds sends a POST with a JSON body and three headers.

HeaderDescription
Reeds-Event-IdThe event id, for example evt_4fQ7Hd2X. Use it to skip duplicates.
Reeds-Event-TypeThe event type, for example delivery.status_changed.
Reeds-Signaturet=<unix time>,v1=<hex>. See Verify signatures.

Every event has the same envelope:

Event envelope
{
"id": "evt_4fQ7Hd2X",
"type": "delivery.status_changed",
"created_at": "2026-10-26T13:05:02+01:00",
"livemode": true,
"data": {
"previous_status": "rider_assigned",
"delivery": { "id": "dlv_9Kd3Qx7M", "status": "picked_up", "...": "the full delivery object" }
}
}

livemode tells you which mode produced the event. See Events for every type and its data.

Reply fast​

Reply with any 2xx status within 10 seconds. Anything else (a non-2xx status, a timeout, a connection error) counts as a failed attempt.

tip

Verify the signature, save the event, return 200, and do the real work afterwards in a queue. Slow handlers cause timeouts, and timeouts cause retries.

Retries​

If an attempt fails, Reeds retries on this schedule:

RetryWait after the previous attempt
11 minute
25 minutes
330 minutes
42 hours
56 hours
612 hours
724 hours

Each wait also gets up to 15 seconds of random jitter, so a burst of failures doesn't come back as a burst. That is about 2 days in all: 8 attempts. After the last one the event is marked failed. Every attempt is signed afresh with the time it is sent, so a retry passes the five-minute timestamp check.

An endpoint that keeps failing for 3 days is disabled. Events still waiting to be sent are marked failed, and the account's Owners, Admins and Developers are emailed. Fix the endpoint, then save its URL again in Dashboard → Developers to turn it back on, and resend the failed events you need from the event log.

Reeds can also turn an endpoint off, for example while investigating a problem with it; your audit log says why. Nothing is sent to it then, not even a resend or a test event, and saving its URL doesn't turn it back on: contact support and Reeds turns it back on.

Event log and Resend​

Dashboard → Developers shows the event log: every event, every attempt, the response your server gave. Use the Resend button to send an event again, for example after you fix a bug. A resend is one extra attempt, right away: it doesn't change the retry schedule, and it works on an endpoint disabled after failing, without turning it back on. The log keeps events for 30 days.

Duplicates and ordering​

  • Duplicates. An event can arrive more than once. Store the ids you have processed (Reeds-Event-Id) and skip repeats.
  • Order. Events can arrive out of order. For delivery events, compare data.delivery.updated_at with what you already hold and ignore an older one. Or fetch the delivery with GET /deliveries/{id}.

Send a test event​

POST /webhooks/test sends a test event to your endpoint, so you can check your receiver without creating a delivery. It is signed like a real event and has its own evt_ id. Send type to choose the event (delivery.status_changed by default).

curl -X POST https://api.getreeds.co/shipping/v1/webhooks/test \
-H "Authorization: Bearer $REEDS_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type": "wallet.funded"}'

The answer's data is {"event_id": "evt_...", "type": "wallet.funded", "sent": true}. sent is false when your endpoint didn't answer 2xx within 10 seconds. A test event appears in the event log, but it is sent once and never retried. With no endpoint saved for the mode, or a type that isn't an event type, you get 422 INVALID_REQUEST; while Reeds has turned the endpoint off, 409 ACTION_NOT_ALLOWED. You can send 10 test events a minute.

A minimal receiver​

Each example verifies the signature on the raw body, replies quickly and hands the work to a queue. The verify helper is explained in Verify signatures.

server.js
import express from 'express';
import crypto from 'node:crypto';

const app = express();
const SECRET = process.env.REEDS_WEBHOOK_SECRET; // whsec_...

function verify(rawBody, header) {
const parts = header.split(',').map((p) => p.split('='));
const t = parts.find(([k]) => k === 't')?.[1];
const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${rawBody}`).digest('hex');
return sigs.some((s) => s.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

// express.raw keeps the body exactly as Reeds sent it.
app.post('/webhooks/reeds', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!verify(raw, req.get('Reeds-Signature') || '')) return res.sendStatus(400);

const event = JSON.parse(raw);
queue.add('reeds-event', event); // your queue: process later, skip ids you've seen
res.sendStatus(200);
});

app.listen(3000);

Next steps​