curl https://api.kwugwo.africa/v1/refunds \
-H "Authorization: Bearer $KWUGWO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"ugwo_activity": "act.VCvr.qnbBJaElzSTHOcaTzxbuDlIs",
"amount": 200000,
"ref": "order_4719_refund_1",
"reason": "Customer returned the notebook",
"meta": { "order_id": "4719", "agent": "ada@shop.example" }
}'
const res = await fetch("https://api.kwugwo.africa/v1/refunds", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.KWUGWO_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
ugwo_activity: activityUid,
amount: 200000, // omit to refund everything left
ref: `${orderId}_refund_1`, // idempotency key
reason: "Customer returned the notebook",
}),
});
const refund = await res.json();
if (res.status === 409) {
// Already refunded with this ref: safe to ignore on a retry.
} else if (refund.status === "failed") {
console.error(refund.error.code, refund.error.message);
}
<?php
$ch = curl_init("https://api.kwugwo.africa/v1/refunds");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('KWUGWO_SECRET_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'ugwo_activity' => $activityUid,
'amount' => 200000,
'ref' => $orderId . '_refund_1',
'reason' => 'Customer returned the notebook',
]),
]);
$refund = json_decode(curl_exec($ch), true);
curl_close($ch);
import os, requests
res = requests.post(
"https://api.kwugwo.africa/v1/refunds",
headers={"Authorization": f"Bearer {os.environ['KWUGWO_SECRET_KEY']}"},
json={
"ugwo_activity": activity_uid,
"amount": 200000,
"ref": f"{order_id}_refund_1",
"reason": "Customer returned the notebook",
},
)
refund = res.json()
if refund.get("status") == "failed":
print(refund["error"]["code"], refund["error"]["message"])
body, _ := json.Marshal(map[string]any{
"ugwo_activity": activityUid,
"amount": 200000,
"ref": orderId + "_refund_1",
"reason": "Customer returned the notebook",
})
req, _ := http.NewRequest("POST", "https://api.kwugwo.africa/v1/refunds", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("KWUGWO_SECRET_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
{
"uid": "rfd.VCvr.Hs7dQmXk2LpRvN9tYbCwZaEf",
"psp_external_ref": "3018284",
"ref": "order_4719_refund_1",
"reason": "Customer returned the notebook",
"amount": 200000,
"status": "processing",
"error": null,
"created_at": "2026-08-12T10:04:11+00:00",
"updated_at": "2026-08-12T10:04:12+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"
},
"nzube_psp": null
}
{
"uid": "rfd.VCvr.Pq3vLmTn8XcBd5RyWkJhZsAu",
"psp_external_ref": null,
"ref": "order_4719_refund_2",
"reason": "Duplicate charge",
"amount": 100000,
"status": "failed",
"error": {
"code": "psp_error",
"message": "Transaction has been fully refunded"
},
"created_at": "2026-08-12T10:09:02+00:00",
"updated_at": "2026-08-12T10:09:03+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:09:03+00:00"
},
"nzube_psp": null
}
{
"message": "nzube.refund.error.refund_amount_too_high",
"code": "not_allowed",
"params": []
}
{
"message": "nzube.refund.error.ref_already_exists",
"code": "already_exists",
"params": []
}
{
"type": "validation.error",
"title": "An error occurred",
"status": 422,
"detail": "This value should be 1 or more.",
"message": "Validation failed",
"errors": [
{ "field": "amount", "message": "This value should be 1 or more." }
]
}
Refunds
Create a refund
Refund all or part of a successful charge.
POST
/
v1
/
refunds
curl https://api.kwugwo.africa/v1/refunds \
-H "Authorization: Bearer $KWUGWO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"ugwo_activity": "act.VCvr.qnbBJaElzSTHOcaTzxbuDlIs",
"amount": 200000,
"ref": "order_4719_refund_1",
"reason": "Customer returned the notebook",
"meta": { "order_id": "4719", "agent": "ada@shop.example" }
}'
const res = await fetch("https://api.kwugwo.africa/v1/refunds", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.KWUGWO_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
ugwo_activity: activityUid,
amount: 200000, // omit to refund everything left
ref: `${orderId}_refund_1`, // idempotency key
reason: "Customer returned the notebook",
}),
});
const refund = await res.json();
if (res.status === 409) {
// Already refunded with this ref: safe to ignore on a retry.
} else if (refund.status === "failed") {
console.error(refund.error.code, refund.error.message);
}
<?php
$ch = curl_init("https://api.kwugwo.africa/v1/refunds");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('KWUGWO_SECRET_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'ugwo_activity' => $activityUid,
'amount' => 200000,
'ref' => $orderId . '_refund_1',
'reason' => 'Customer returned the notebook',
]),
]);
$refund = json_decode(curl_exec($ch), true);
curl_close($ch);
import os, requests
res = requests.post(
"https://api.kwugwo.africa/v1/refunds",
headers={"Authorization": f"Bearer {os.environ['KWUGWO_SECRET_KEY']}"},
json={
"ugwo_activity": activity_uid,
"amount": 200000,
"ref": f"{order_id}_refund_1",
"reason": "Customer returned the notebook",
},
)
refund = res.json()
if refund.get("status") == "failed":
print(refund["error"]["code"], refund["error"]["message"])
body, _ := json.Marshal(map[string]any{
"ugwo_activity": activityUid,
"amount": 200000,
"ref": orderId + "_refund_1",
"reason": "Customer returned the notebook",
})
req, _ := http.NewRequest("POST", "https://api.kwugwo.africa/v1/refunds", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("KWUGWO_SECRET_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
{
"uid": "rfd.VCvr.Hs7dQmXk2LpRvN9tYbCwZaEf",
"psp_external_ref": "3018284",
"ref": "order_4719_refund_1",
"reason": "Customer returned the notebook",
"amount": 200000,
"status": "processing",
"error": null,
"created_at": "2026-08-12T10:04:11+00:00",
"updated_at": "2026-08-12T10:04:12+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"
},
"nzube_psp": null
}
{
"uid": "rfd.VCvr.Pq3vLmTn8XcBd5RyWkJhZsAu",
"psp_external_ref": null,
"ref": "order_4719_refund_2",
"reason": "Duplicate charge",
"amount": 100000,
"status": "failed",
"error": {
"code": "psp_error",
"message": "Transaction has been fully refunded"
},
"created_at": "2026-08-12T10:09:02+00:00",
"updated_at": "2026-08-12T10:09:03+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:09:03+00:00"
},
"nzube_psp": null
}
{
"message": "nzube.refund.error.refund_amount_too_high",
"code": "not_allowed",
"params": []
}
{
"message": "nzube.refund.error.ref_already_exists",
"code": "already_exists",
"params": []
}
{
"type": "validation.error",
"title": "An error occurred",
"status": 422,
"detail": "This value should be 1 or more.",
"message": "Validation failed",
"errors": [
{ "field": "amount", "message": "This value should be 1 or more." }
]
}
Refunds money from a successful activity
back to the payer. The refund is submitted to the provider before the
call returns, so
status comes back as processing, or failed if
the provider refused it outright.
Read Refunds first, in particular how much you
can refund and
idempotency.
The activity must be
successful or partially_refunded. A failed
charge, one still waiting on the customer, or a fully refunded
one is rejected with 405.Body parameters
string
required
The activity to refund,
act.XXXX.YYYY.... Must belong to your
workspace and hold settled money.integer
Amount to refund, in the smallest currency unit (kobo for NGN).
Omit it to refund everything still refundable on the activity.Must be at least the currency minimum (
20000 = ₦200.00 for NGN),
and cannot take the total past what the activity collected.string
Your own reference, 1-150 characters. Unique per workspace, so
reusing one returns
409. Strongly recommended on every refund:
it is your idempotency key.string
Why you’re refunding, 1-255 characters. Forwarded to the provider
as the merchant note, so it shows up in their dashboard too.
string
Provider connection to refund through,
psp.XXXX.YYYY.... Defaults
to the connection that took the payment. It must be active and
belong to the same provider as the original charge.object
The customer’s bank account to send the money to. Required by some
PSPs for
bank_transfer and ussd payments, where the original
payer account isn’t known; without it the refund fails. See
Refunds for bank transfer and USSD payments.object
Up to 50 string key/value pairs (keys ≤ 100 chars, values ≤ 255;
null values are dropped). Stored against the refund for your own
bookkeeping. The merchant API does not return it.Response
The Refund object. The fields worth branching on immediately:string
Refund ID,
rfd.XXXX.YYYY.... Persist it: every refund webhook
identifies itself with this.string
processing on the happy path, failed if the provider refused it
outright. Do not treat processing as money returned; wait for
refund.successful.object | null
Populated when
status is failed.object
The activity, already updated. Read
refunded_amount here to see
how much of the payment is now committed to refunds.Errors
| Status | Cause |
|---|---|
405 | The refund isn’t allowed in the payment’s current state: the activity never succeeded, the amount is below the minimum or above what’s left, or the provider connection is inactive or belongs to a different provider. The message says which. |
409 | A refund with this ref already exists on the workspace. |
422 | ugwo_activity unknown, amount below 1, ref or reason too long, or malformed meta. |
500 | Unexpected failure. The refund may or may not exist, so list refunds filtered by the activity before retrying. |
A
failed refund is a 200 response, not an HTTP error. A
provider declining a refund is a normal outcome, so it comes back
as a refund object with "status": "failed" and a populated
error. Check status, not just the HTTP code.curl https://api.kwugwo.africa/v1/refunds \
-H "Authorization: Bearer $KWUGWO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"ugwo_activity": "act.VCvr.qnbBJaElzSTHOcaTzxbuDlIs",
"amount": 200000,
"ref": "order_4719_refund_1",
"reason": "Customer returned the notebook",
"meta": { "order_id": "4719", "agent": "ada@shop.example" }
}'
const res = await fetch("https://api.kwugwo.africa/v1/refunds", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.KWUGWO_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
ugwo_activity: activityUid,
amount: 200000, // omit to refund everything left
ref: `${orderId}_refund_1`, // idempotency key
reason: "Customer returned the notebook",
}),
});
const refund = await res.json();
if (res.status === 409) {
// Already refunded with this ref: safe to ignore on a retry.
} else if (refund.status === "failed") {
console.error(refund.error.code, refund.error.message);
}
<?php
$ch = curl_init("https://api.kwugwo.africa/v1/refunds");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('KWUGWO_SECRET_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'ugwo_activity' => $activityUid,
'amount' => 200000,
'ref' => $orderId . '_refund_1',
'reason' => 'Customer returned the notebook',
]),
]);
$refund = json_decode(curl_exec($ch), true);
curl_close($ch);
import os, requests
res = requests.post(
"https://api.kwugwo.africa/v1/refunds",
headers={"Authorization": f"Bearer {os.environ['KWUGWO_SECRET_KEY']}"},
json={
"ugwo_activity": activity_uid,
"amount": 200000,
"ref": f"{order_id}_refund_1",
"reason": "Customer returned the notebook",
},
)
refund = res.json()
if refund.get("status") == "failed":
print(refund["error"]["code"], refund["error"]["message"])
body, _ := json.Marshal(map[string]any{
"ugwo_activity": activityUid,
"amount": 200000,
"ref": orderId + "_refund_1",
"reason": "Customer returned the notebook",
})
req, _ := http.NewRequest("POST", "https://api.kwugwo.africa/v1/refunds", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("KWUGWO_SECRET_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
{
"uid": "rfd.VCvr.Hs7dQmXk2LpRvN9tYbCwZaEf",
"psp_external_ref": "3018284",
"ref": "order_4719_refund_1",
"reason": "Customer returned the notebook",
"amount": 200000,
"status": "processing",
"error": null,
"created_at": "2026-08-12T10:04:11+00:00",
"updated_at": "2026-08-12T10:04:12+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"
},
"nzube_psp": null
}
{
"uid": "rfd.VCvr.Pq3vLmTn8XcBd5RyWkJhZsAu",
"psp_external_ref": null,
"ref": "order_4719_refund_2",
"reason": "Duplicate charge",
"amount": 100000,
"status": "failed",
"error": {
"code": "psp_error",
"message": "Transaction has been fully refunded"
},
"created_at": "2026-08-12T10:09:02+00:00",
"updated_at": "2026-08-12T10:09:03+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:09:03+00:00"
},
"nzube_psp": null
}
{
"message": "nzube.refund.error.refund_amount_too_high",
"code": "not_allowed",
"params": []
}
{
"message": "nzube.refund.error.ref_already_exists",
"code": "already_exists",
"params": []
}
{
"type": "validation.error",
"title": "An error occurred",
"status": 422,
"detail": "This value should be 1 or more.",
"message": "Validation failed",
"errors": [
{ "field": "amount", "message": "This value should be 1 or more." }
]
}