> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kwugwo.africa/llms.txt
> Use this file to discover all available pages before exploring further.

# Refund webhooks

> The four refund events, what each one carries, and how to handle them safely.

A refund settles asynchronously, so webhooks are how you learn the
outcome. See [Webhook deliveries](https://docs.kwugwo.africa/webhook-deliveries.md) for the
transport, signing, and retry rules that apply to every event.

## The four events

| Event | Fires when | Typical `data.status` |
| - | - | - |
| `refund.created` | The refund is recorded, before it reaches the provider. | `pending` |
| `refund.updated` | Any field on the refund changes. | anything |
| `refund.successful` | The refund settles. | `successful` |
| `refund.failed` | The refund is rejected. | `failed` |

A single state change produces two deliveries: the broad
`refund.updated` and the specific `refund.successful` or
`refund.failed`.

> [!TIP]
>
> Drive business logic from `refund.successful` and `refund.failed`,
> and keep `refund.updated` as an audit feed.

Deliveries are queued, so they can arrive out of order, and
`refund.created` can reach you before your `POST /v1/refunds` call
returns. Decide from `data.status`, not from which event landed
first.

## Payload

The envelope is the standard [webhook
envelope](https://docs.kwugwo.africa/webhook-deliveries.md#the-event-object). `data` is the [Refund
object](https://docs.kwugwo.africa/refunds/object.md), exactly as
[`GET /v1/refunds/{refundUid}`](https://docs.kwugwo.africa/refunds/get) returns it.

```json refund.successful theme={"system"}
{
  "uid": "evt.VCvr.7K2qPmRtV9xLnQ8sD1cYwHfE",
  "event": "refund.successful",
  "data": {
    "uid": "rfd.VCvr.Hs7dQmXk2LpRvN9tYbCwZaEf",
    "psp_external_ref": "3018284",
    "ref": "order_4719_refund_1",
    "reason": "Customer returned the notebook",
    "amount": 200000,
    "status": "successful",
    "error": null,
    "created_at": "2026-08-12T10:04:11+00:00",
    "updated_at": "2026-08-12T14:31:07+00:00",
    "ugwo": {
      "uid": "ugw.VCvr.7K2qPmRtV9xLnQ8sD1cYwHfE",
      "currency": "NGN"
    },
    "ugwo_activity": {
      "uid": "act.VCvr.qnbBJaElzSTHOcaTzxbuDlIs",
      "activity_type": "charge",
      "status": "partially_refunded",
      "psp": "paystack_ng",
      "psp_external_id": "5kfxw34ugz",
      "amount": 500000,
      "charged_amount": 500000,
      "refunded_amount": 200000,
      "error": null,
      "created_at": "2026-08-11T19:55:50+00:00",
      "updated_at": "2026-08-12T10:04:11+00:00"
    }
  },
  "changed_fields": null,
  "created_at": "2026-08-12T14:31:07+00:00"
}
```

`refund.updated` is the only refund event that carries
`changed_fields`, a map of each changed key to its previous value.
Together with `data` it gives you both sides of the transition:

```json theme={"system"}
{
  "event": "refund.updated",
  "data": { "status": "successful", "psp_external_ref": "3018284", "...": "..." },
  "changed_fields": { "status": "processing" }
}
```

### Failures

On `refund.failed`, `error` tells you whether a retry is worth it.

```json theme={"system"}
{
  "event": "refund.failed",
  "data": {
    "uid": "rfd.VCvr.Pq3vLmTn8XcBd5RyWkJhZsAu",
    "amount": 100000,
    "status": "failed",
    "error": {
      "code": "psp_error",
      "message": "Transaction has been fully refunded"
    },
    "...": "..."
  },
  "changed_fields": null
}
```

| `error.code` | Meaning | Retry? |
| - | - | - |
| `psp_error` | The provider refused, and `message` is their wording. | Only after fixing the cause. |
| `system_error` | The request could not be completed with the provider. | Yes, once the cause is cleared. |

## Related events

A refund also moves money on the payment, so expect
`ugwo.activity.updated` and `ugwo.updated` alongside the refund
events whenever `refunded_amount` or a status changes. See [what a
refund does to the
payment](https://docs.kwugwo.africa/refunds/overview.md#what-a-refund-does-to-the-payment).

## Handling refunds safely

* **Acknowledge fast.** Verify `X-Kwugwo-Signature`, queue the event,
  return `200`. A slow handler burns one of your [three delivery
  attempts](https://docs.kwugwo.africa/webhook-deliveries.md#retries).
* **Deduplicate on the envelope `uid`.** Retries reuse it. Note that
  `refund.updated` and `refund.successful` describe the same
  transition under different envelope IDs, so dedupe protects you
  from replays, not from the two-event pattern.
* **Make the effect idempotent on `data.uid`.** Key your bookkeeping
  on the refund ID so a credit happens once, however many events
  mention it.
* **Re-fetch before anything expensive.** `data` is a snapshot from
  when the event fired; pull [`GET
  /v1/refunds/{uid}`](https://docs.kwugwo.africa/refunds/get.md) if your worker runs later.
* **Accept unfamiliar refunds.** Refunds [created outside the
  API](https://docs.kwugwo.africa/refunds/overview.md#refunds-created-outside-the-api) arrive with
  `ref: null` for refunds you never requested. Record them.

```js Node (Express) theme={"system"}
app.post("/webhooks/kwugwo", async (req, res) => {
  if (!verifyKwugwoSignature(req.rawBody, req.get("X-Kwugwo-Signature"), SECRET)) {
    return res.sendStatus(401);
  }

  const { uid, event, data } = JSON.parse(req.rawBody);

  if (await seenEvent(uid)) return res.sendStatus(200);
  await markEventSeen(uid);

  res.sendStatus(200); // acknowledge, then work

  switch (event) {
    case "refund.successful":
      // Idempotent on the refund uid, not the event uid.
      await closeRefund(data.uid, {
        amount: data.amount,
        ref: data.ref,
        pspRef: data.psp_external_ref,
        activityUid: data.ugwo_activity.uid,
      });
      break;

    case "refund.failed":
      await flagRefundForReview(data.uid, data.error.code, data.error.message);
      break;

    default:
      await recordRefundEvent(event, data);
  }
});
```

> [!WARNING]
>
> Never credit a customer on `refund.created` or on a `processing`
> status. Only `successful` means the money has gone back.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.