TagPayTagPay

Guides

Refunds

Return funds to a customer after a successful charge. Refunds are first-class objects — they have their own ID, their own status lifecycle, and their own webhook events. You can issue the full amount or a partial slice, and you can issue multiple partial refunds against the same charge up to the original total.

When you can refund

Refunds are only valid for charges where status is success. Pending and failed charges have nothing to refund — there is no captured money on TagPay's side.

The eligibility window is 90 days from the original paid_at timestamp. After that, the funds have already settled through the card scheme and the refund channel is closed. For older disputes, raise a chargeback via the Disputes flow instead.

  • Charge must have status: success.
  • Original paid_at must be within the last 90 days.
  • Remaining refundable balance must be greater than zero.

Full vs partial

Omit amount in the request body to refund the full remaining balance of the charge. This is the common case — a customer cancels an order, you give them their money back.

# Full refund — omit amount
curl https://gwt.tagpay.ng/v1/refunds \
-H "Authorization: Bearer $TAGPAY_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"charge_reference": "tp_charge_8mFp2Lq9",
"reason": "Customer cancelled order"
}'

For a partial refund, specify amount in kobo. You can issue multiple partial refunds against the same charge until you exhaust the original total. TagPay tracks the remaining refundable balance — if you try to refund more than is available, you get REFUND_EXCEEDS_BALANCE.

# Partial — refund NGN 2,000 of a NGN 5,000 charge
curl https://gwt.tagpay.ng/v1/refunds \
-H "Authorization: Bearer $TAGPAY_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"charge_reference": "tp_charge_8mFp2Lq9",
"amount": 200000,
"reason": "Defective item replaced"
}'

Timing

TagPay accepts the refund instantly. The customer sees the funds back on their end on a timeline that depends on the original payment channel:

Original channelCustomer visibility
CardSame day — usually within 2–6 hours
Bank transferT+1 banking day via NIBSS reversal
USSDT+2 banking days

Status transitions from pending to success when the customer's bank confirms credit. Subscribe to webhooks to react when that happens — do not poll.

Webhooks

Two events fire for every refund — listen for both. Both deliver a snapshot of the original charge, not a dedicated refund object — there is no refund id and no charge_reference field on the wire. Reconcile using the top-level reference (the charge's own reference); if a single charge has multiple refunds in flight, correlate against your own stored request records, since the webhook body alone can't distinguish which refund attempt it is.

  • refund.success — funds confirmed back to the customer. Safe to mark the order as refunded in your system and notify the buyer.
  • refund.failed — the bank rejected the reversal (closed account, stale card, NIBSS timeout). The original charge is still captured on TagPay's side; reach out to the customer for an alternative. Note that data.status still reads success here too — it's the underlying charge's status, unaffected by the refund failing.
// Example payload — refund.success
// (refund.failed carries this same shape; only the event name differs)
{
"event": "refund.success",
"data": {
"id": "7f3b9c2e-4a1d-4e6f-9b8a-1c2d3e4f5a6b",
"reference": "tp_charge_8mFp2Lq9",
"gatewayReference": "gw_bf3a7c1e9d2b4f5a",
"amount": 250000,
"currency": "NGN",
"fee": 13750,
"netAmount": 236250,
"status": "success",
"paymentMethod": "bank_transfer",
"cardBrand": null,
"cardLast4": null,
"bankName": "Guaranty Trust Bank",
"accountNumber": "0123456789",
"senderName": "Adaeze Okafor",
"senderBankName": "Guaranty Trust Bank",
"senderAccountNumber": "0123456789",
"destinationBankName": "Providus Bank",
"destinationAccountNumber": "9988776655",
"destinationAccountName": "TAGPAY-ZENITH TRADERS LTD",
"collectionAccount": {
"id": "c1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
"reference": "zt8x9k2m_PRIMARY",
"accountNumber": "9988776655",
"accountName": "TAGPAY-ZENITH TRADERS LTD"
},
"paidAt": "2026-07-15T09:42:11.000Z",
"processedAt": "2026-07-15T09:42:11.000Z",
"createdAt": "2026-07-15T09:41:58.000Z"
}
}

Note what's not here: no refunded amount, no refund reason, and — for refund.failed — no failure reason. amount is the original charge amount, not what was refunded. Fetch GET /v1/refunds/:id for those details.

See the Webhooks guide for delivery semantics and signature verification.

Errors

The full list lives in the Errors catalog. The two refund-specific codes to know:

  • REFUND_EXCEEDS_BALANCE — your amount is greater than the remaining refundable balance for that charge. Fetch the charge to see what's left.
  • CHARGE_NOT_REFUNDABLE — the charge never succeeded, or it succeeded more than 90 days ago. There's nothing for TagPay to send back.

See the full Refunds API reference for every parameter and response field.