Webhook events
Every event uses the same envelope: id, type, created_at, livemode and data. This page lists each type and what data carries.
Event types
| Event | When it fires | What is in data |
|---|---|---|
delivery.created | A delivery or draft was created | delivery |
delivery.requires_action | An action was added to required_actions | required_action (the one just added) and delivery, with every outstanding action |
delivery.booked | The delivery was priced and charged (or added to the invoice) | delivery |
delivery.status_changed | Any status change | delivery and previous_status |
delivery.schedule_changed | pickup_by or deliver_by moved | delivery, reason and previous_schedule |
delivery.expiring | A draft or an unready delivery will be cancelled soon (reminders on day 3 and day 6) | delivery and expires_at |
delivery.cancelled | The delivery was cancelled | cancellation and delivery (which carries the same block) |
address.confirmed | An address went from needs_confirmation to verified | address |
wallet.funded | A top-up landed | amount, currency, the new balance, the top-up's reference and the wallet transaction |
wallet.low_balance | The balance fell to or below your threshold | The wallet: balance, currency and low_balance_threshold |
invoice.issued | A monthly invoice was issued | invoice |
invoice.paid | An invoice was paid in full (a payment, a transfer Reeds recorded, or a credit that settled it) | invoice |
invoice.overdue | An invoice passed its due date unpaid; the account is on hold until it is paid | invoice |
delivery is always the full delivery object, the same one GET /deliveries/{id} returned at the moment of the event. The type's own fields (previous_status, required_action, cancellation, expires_at, reason, previous_schedule) come before it.
Reeds adds event types without a new API version. If you receive a type you don't know, reply 200 and ignore it. Don't return an error, or Reeds will retry it for two days.
Example payloads
The delivery objects below are shortened with "...": "the full delivery object". Real events carry every field.
delivery.status_changed
Sent on every status change. previous_status is the status before this one.
{
"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",
"tracking_code": "HQ3W5F2",
"updated_at": "2026-10-26T13:05:00+01:00",
"...": "the full delivery object"
}
}
}
Compare delivery.updated_at with what you already hold, because events can arrive out of order.
delivery.requires_action
Sent when a delivery is blocked, for example a draft whose pickup address needs confirmation.
{
"id": "evt_6Rt1Mz8B",
"type": "delivery.requires_action",
"created_at": "2026-10-26T10:20:03+01:00",
"livemode": true,
"data": {
"required_action": {
"type": "CONFIRM_PICKUP_ADDRESS",
"address_id": "adr_3Mz8Rt1Q",
"message": "Pickup address needs confirmation"
},
"delivery": {
"id": "dlv_8Pq4Ns2L",
"status": "requires_action",
"required_actions": [
{
"type": "CONFIRM_PICKUP_ADDRESS",
"address_id": "adr_3Mz8Rt1Q",
"message": "Pickup address needs confirmation"
}
],
"...": "the full delivery object"
}
}
}
See Drafts and required actions for every action type.
delivery.schedule_changed
Sent when Reeds expects to miss the promise and moves pickup_by or deliver_by. reason is a short note for people, so show it but don't branch on it. previous_schedule is the range you had. The new deliver_by is the whole promise, so earliest_delivery is null.
{
"id": "evt_9Pw2Hs4L",
"type": "delivery.schedule_changed",
"created_at": "2026-10-27T09:10:00+01:00",
"livemode": true,
"data": {
"reason": "Rider delayed by flooding",
"previous_schedule": {
"pickup_by": "2026-10-26T19:00:00+01:00",
"deliver_by": "2026-10-27T19:00:00+01:00",
"earliest_delivery": "2026-10-26T19:00:00+01:00"
},
"delivery": {
"id": "dlv_9Kd3Qx7M",
"status": "picked_up",
"schedule": {
"pickup_by": "2026-10-26T19:00:00+01:00",
"deliver_by": "2026-10-28T19:00:00+01:00",
"earliest_delivery": null
},
"...": "the full delivery object"
}
}
}
Update the date you show your customer from data.delivery.schedule.deliver_by.
delivery.cancelled
The cancellation block says why and what was refunded. reason is business, expired, processing_failed or reeds; refunded_amount is in kobo.
{
"id": "evt_2Wn5Kc9T",
"type": "delivery.cancelled",
"created_at": "2026-10-26T11:02:10+01:00",
"livemode": true,
"data": {
"cancellation": {
"reason": "business",
"cancelled_by": "api",
"note": null,
"refunded_amount": 280000,
"cancelled_at": "2026-10-26T11:02:09+01:00"
},
"delivery": {
"id": "dlv_9Kd3Qx7M",
"status": "cancelled",
"cancellable": false,
"cancellation": {
"reason": "business",
"cancelled_by": "api",
"note": null,
"refunded_amount": 280000,
"cancelled_at": "2026-10-26T11:02:09+01:00"
},
"...": "the full delivery object"
}
}
}
address.confirmed
{
"id": "evt_8Jd3Pq6V",
"type": "address.confirmed",
"created_at": "2026-10-26T10:30:00+01:00",
"livemode": true,
"data": {
"address": {
"id": "adr_3Mz8Rt1Q",
"status": "verified",
"serviceable": true,
"...": "the full address object"
}
}
}
wallet.funded
{
"id": "evt_7Lm3Qs1R",
"type": "wallet.funded",
"created_at": "2026-10-26T14:10:00+01:00",
"livemode": true,
"data": {
"amount": 5000000,
"currency": "NGN",
"balance": 5000000,
"reference": "fund-8f3a1c7e5b2d4a6f9e0c",
"transaction": {
"id": "txn_Rt7Bn1Qk",
"livemode": true,
"type": "credit",
"amount": 5000000,
"currency": "NGN",
"balance_after": 5000000,
"delivery_id": null,
"description": "Wallet funding (Paystack)",
"created_at": "2026-10-26T14:10:00+01:00"
}
}
}
reference is the one POST /wallet/fund returned, so you can match the webhook to the top-up you started. A test top-up from POST /sandbox/wallet/fund has a reference starting test-topup-. That endpoint doesn't return one, so you can't match it in advance. Other credits can have reference: null, so don't assume it is there.
wallet.low_balance
{
"id": "evt_5Bz2Ng7H",
"type": "wallet.low_balance",
"created_at": "2026-10-26T14:40:00+01:00",
"livemode": true,
"data": {
"balance": 450000,
"currency": "NGN",
"low_balance_threshold": 500000
}
}
Money is in kobo, so 450000 is ₦4,500.
It is sent once, by the debit that takes the balance to or below your threshold (a batch of bookings that crosses it sends one). It isn't sent again until the balance has risen back to the threshold plus a margin (₦1,000 or 10% of the threshold, whichever is more) and fallen again, and at most once an hour. Changing the threshold re-arms it without sending one. Test mode sends it too.
invoice.issued
Sent on the 1st of the month for the previous month, to platform accounts on monthly invoicing. invoice.paid and invoice.overdue carry the same invoice object. See Invoices.
{
"id": "evt_3Kq8Vb6N",
"type": "invoice.issued",
"created_at": "2026-10-01T06:00:05+01:00",
"livemode": true,
"data": {
"invoice": {
"id": "inv_6Gc4Yt8N",
"number": "RDS-2026-000123",
"status": "issued",
"period": "2026-09",
"total": 108200000,
"amount_due": 108200000,
"currency": "NGN",
"issued_at": "2026-10-01T06:00:00+01:00",
"due_at": "2026-10-15T23:59:59+01:00",
"...": "the full invoice object"
}
}
}
Handling events
- Dedupe on the event
id. - Branch on
type, with a default branch that ignores unknown types. - Fetch the delivery with
GET /deliveries/{id}if you doubt an event's order.