Skip to main content

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​

EventWhen it firesWhat is in data
delivery.createdA delivery or draft was createddelivery
delivery.requires_actionAn action was added to required_actionsrequired_action (the one just added) and delivery, with every outstanding action
delivery.bookedThe delivery was priced and charged (or added to the invoice)delivery
delivery.status_changedAny status changedelivery and previous_status
delivery.schedule_changedpickup_by or deliver_by moveddelivery, reason and previous_schedule
delivery.expiringA draft or an unready delivery will be cancelled soon (reminders on day 3 and day 6)delivery and expires_at
delivery.cancelledThe delivery was cancelledcancellation and delivery (which carries the same block)
address.confirmedAn address went from needs_confirmation to verifiedaddress
wallet.fundedA top-up landedamount, currency, the new balance, the top-up's reference and the wallet transaction
wallet.low_balanceThe balance fell to or below your thresholdThe wallet: balance, currency and low_balance_threshold
invoice.issuedA monthly invoice was issuedinvoice
invoice.paidAn invoice was paid in full (a payment, a transfer Reeds recorded, or a credit that settled it)invoice
invoice.overdueAn invoice passed its due date unpaid; the account is on hold until it is paidinvoice

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.

Tolerate unknown types

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.

delivery.status_changed
{
"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.

delivery.requires_action
{
"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.

delivery.schedule_changed
{
"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.

delivery.cancelled
{
"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​

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​

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​

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.

invoice.issued
{
"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.

Next steps​