Refunds
Send all or part of a virtual account deposit back to the customer who paid it.
How refunds work
- The refund amount is debited from your Gafiapay balance as soon as the refund is accepted. Your balance must cover it.
- A refund starts as
pending. It becomescompletedonce the money reaches the customer, usually within 1–3 business days. - If a refund ends as
failed, the amount is credited back to your balance and the payment becomes refundable again. - Each payment can have one refund, for the full amount or part of it.
- Once a refund is created, the original payment's status changes to
refunded.
A partial refund uses up the payment's refund. You can't refund the rest of the amount later unless the first refund fails.
| Status | Meaning |
|---|---|
pending | Accepted and being processed. |
completed | The customer has been paid back. completedAt is set. |
failed | The refund didn't go through. The amount is back in your balance, and failedAt is set. |
Create a refund
Start a refund for a completed deposit. Identify the deposit with one of orderNo or transactionId.
POST requests need an x-signature computed over the JSON body and timestamp. See Authentication.
Body
orderNo string one of the two orderNo, from the payment.received webhook.transactionId string one of the two id, also in the payment.received webhook.amount number required reason string required The response returns 201. Save refund.orderId: you need it to check the refund's status.
Errors
| Status | When |
|---|---|
400 | Something in the request is wrong: no deposit reference (or both sent), an invalid amount, no reason, an amount above the original deposit, or a balance too low to cover it. Also returned when the bank rejects the refund. |
404 | No completed payment matches the identifier for your business. This includes payments that have already been refunded. |
409 | Another refund for your business is being processed right now. Retry after a few seconds. |
502 | Couldn't reach the bank. No money was moved, so you can safely retry. |
curl -X POST 'https://api.gafiapay.com/api/v1/external/refund/create' \
-H 'Content-Type: application/json' \
-H 'x-api-key: your_api_key' \
-H 'x-timestamp: 1678901234567' \
-H 'x-signature: generated_signature' \
-d '{
"orderNo": "PP25101400098765",
"amount": 1500,
"reason": "Customer cancelled the order"
}'const crypto = require('crypto');
const body = JSON.stringify({
orderNo: 'PP25101400098765',
amount: 1500,
reason: 'Customer cancelled the order'
});
const timestamp = Date.now().toString();
const signature = crypto
.createHmac('sha256', process.env.GAFIAPAY_SECRET_KEY)
.update(body + timestamp)
.digest('hex');
const res = await fetch('https://api.gafiapay.com/api/v1/external/refund/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.GAFIAPAY_API_KEY,
'x-timestamp': timestamp,
'x-signature': signature,
},
body, // send the exact string you signed
});
const data = await res.json();import hashlib, hmac, json, os, time, requests
body = json.dumps({
"orderNo": "PP25101400098765",
"amount": 1500,
"reason": "Customer cancelled the order"
}, separators=(",", ":")) # compact, no spaces
timestamp = str(int(time.time() * 1000))
signature = hmac.new(
os.environ["GAFIAPAY_SECRET_KEY"].encode(),
(body + timestamp).encode(),
hashlib.sha256,
).hexdigest()
res = requests.post(
"https://api.gafiapay.com/api/v1/external/refund/create",
data=body, # data=, not json=, so the signed string is sent as is
headers={
"Content-Type": "application/json",
"x-api-key": os.environ["GAFIAPAY_API_KEY"],
"x-timestamp": timestamp,
"x-signature": signature,
},
)
data = res.json()<?php
$body = json_encode([
'orderNo' => 'PP25101400098765',
'amount' => 1500,
'reason' => 'Customer cancelled the order'
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$timestamp = (string) round(microtime(true) * 1000);
$signature = hash_hmac('sha256', $body . $timestamp, getenv('GAFIAPAY_SECRET_KEY'));
$ch = curl_init('https://api.gafiapay.com/api/v1/external/refund/create');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-api-key: ' . getenv('GAFIAPAY_API_KEY'),
'x-timestamp: ' . $timestamp,
'x-signature: ' . $signature,
],
]);
$data = json_decode(curl_exec($ch), true);require 'net/http'
require 'openssl'
require 'json'
body = {
orderNo: 'PP25101400098765',
amount: 1500,
reason: 'Customer cancelled the order'
}.to_json
timestamp = (Time.now.to_f * 1000).to_i.to_s
signature = OpenSSL::HMAC.hexdigest('sha256', ENV['GAFIAPAY_SECRET_KEY'], body + timestamp)
uri = URI('https://api.gafiapay.com/api/v1/external/refund/create')
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['x-api-key'] = ENV['GAFIAPAY_API_KEY']
req['x-timestamp'] = timestamp
req['x-signature'] = signature
req.body = body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
// Compact JSON, sent exactly as signed
String body = "{\"orderNo\":\"PP25101400098765\",\"amount\":1500,\"reason\":\"Customer cancelled the order\"}";
String timestamp = String.valueOf(System.currentTimeMillis());
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(System.getenv("GAFIAPAY_SECRET_KEY").getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = HexFormat.of().formatHex(mac.doFinal((body + timestamp).getBytes(StandardCharsets.UTF_8)));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.gafiapay.com/api/v1/external/refund/create"))
.header("Content-Type", "application/json")
.header("x-api-key", System.getenv("GAFIAPAY_API_KEY"))
.header("x-timestamp", timestamp)
.header("x-signature", signature)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());{
"status": "success",
"data": {
"refund": {
"id": "6710a1c2e4b0f5a3c1d2e3f4",
"orderId": "REF1760500000123A1B2C3D4",
"orderNo": "PP25101500012345",
"amount": 1500,
"formattedAmount": "1500.00",
"currency": "NGN",
"status": "pending",
"reason": "Customer cancelled the order",
"originalTransaction": {
"id": "670f9b11e4b0f5a3c1d2e001",
"orderNo": "PP25101400098765",
"amount": 2000
},
"createdAt": "2026-10-15T09:20:00.000Z"
},
"message": "Refund initiated. The customer will receive funds within 1–3 business days."
}
}{
"status": "fail",
"message": "Insufficient balance to process this refund"
}Get refund status
Get a refund by the orderId returned when you created it. If the refund is still pending, Gafiapay checks with the bank before responding, so the status you get back is current.
GET requests only need x-api-key and x-timestamp.
Path parameters
orderId string required orderId, returned by Create a refund.curl -X GET 'https://api.gafiapay.com/api/v1/external/refund/status/REF1760500000123A1B2C3D4' \
-H 'x-api-key: your_api_key' \
-H 'x-timestamp: 1678901234567'const res = await fetch('https://api.gafiapay.com/api/v1/external/refund/status/REF1760500000123A1B2C3D4', {
headers: {
'x-api-key': process.env.GAFIAPAY_API_KEY,
'x-timestamp': Date.now().toString(),
},
});
const data = await res.json();import os, time, requests
res = requests.get(
"https://api.gafiapay.com/api/v1/external/refund/status/REF1760500000123A1B2C3D4",
headers={
"x-api-key": os.environ["GAFIAPAY_API_KEY"],
"x-timestamp": str(int(time.time() * 1000)),
},
)
data = res.json()<?php
$ch = curl_init('https://api.gafiapay.com/api/v1/external/refund/status/REF1760500000123A1B2C3D4');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('GAFIAPAY_API_KEY'),
'x-timestamp: ' . round(microtime(true) * 1000),
],
]);
$data = json_decode(curl_exec($ch), true);require 'net/http'
require 'json'
uri = URI('https://api.gafiapay.com/api/v1/external/refund/status/REF1760500000123A1B2C3D4')
req = Net::HTTP::Get.new(uri)
req['x-api-key'] = ENV['GAFIAPAY_API_KEY']
req['x-timestamp'] = (Time.now.to_f * 1000).to_i.to_s
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)import java.net.URI;
import java.net.http.*;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.gafiapay.com/api/v1/external/refund/status/REF1760500000123A1B2C3D4"))
.header("x-api-key", System.getenv("GAFIAPAY_API_KEY"))
.header("x-timestamp", String.valueOf(System.currentTimeMillis()))
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());{
"status": "success",
"data": {
"id": "6710a1c2e4b0f5a3c1d2e3f4",
"orderId": "REF1760500000123A1B2C3D4",
"orderNo": "PP25101500012345",
"amount": 1500,
"formattedAmount": "1500.00",
"currency": "NGN",
"status": "completed",
"reason": "Customer cancelled the order",
"originalTransaction": {
"id": "670f9b11e4b0f5a3c1d2e001",
"orderNo": "PP25101400098765",
"amount": 2000
},
"createdAt": "2026-10-15T09:20:00.000Z",
"completedAt": "2026-10-15T11:02:41.000Z"
}
}List refunds
List your business's refunds, newest first.
Query parameters
page number optional Default 1 limit number optional Default 20 100.status string optional pending, completed or failed.curl -X GET 'https://api.gafiapay.com/api/v1/external/refund/list?page=1&limit=20&status=pending' \
-H 'x-api-key: your_api_key' \
-H 'x-timestamp: 1678901234567'const res = await fetch('https://api.gafiapay.com/api/v1/external/refund/list?page=1&limit=20&status=pending', {
headers: {
'x-api-key': process.env.GAFIAPAY_API_KEY,
'x-timestamp': Date.now().toString(),
},
});
const data = await res.json();import os, time, requests
res = requests.get(
"https://api.gafiapay.com/api/v1/external/refund/list?page=1&limit=20&status=pending",
headers={
"x-api-key": os.environ["GAFIAPAY_API_KEY"],
"x-timestamp": str(int(time.time() * 1000)),
},
)
data = res.json()<?php
$ch = curl_init('https://api.gafiapay.com/api/v1/external/refund/list?page=1&limit=20&status=pending');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('GAFIAPAY_API_KEY'),
'x-timestamp: ' . round(microtime(true) * 1000),
],
]);
$data = json_decode(curl_exec($ch), true);require 'net/http'
require 'json'
uri = URI('https://api.gafiapay.com/api/v1/external/refund/list?page=1&limit=20&status=pending')
req = Net::HTTP::Get.new(uri)
req['x-api-key'] = ENV['GAFIAPAY_API_KEY']
req['x-timestamp'] = (Time.now.to_f * 1000).to_i.to_s
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)import java.net.URI;
import java.net.http.*;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.gafiapay.com/api/v1/external/refund/list?page=1&limit=20&status=pending"))
.header("x-api-key", System.getenv("GAFIAPAY_API_KEY"))
.header("x-timestamp", String.valueOf(System.currentTimeMillis()))
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());{
"status": "success",
"data": {
"refunds": [
{
"id": "6710a1c2e4b0f5a3c1d2e3f4",
"orderId": "REF1760500000123A1B2C3D4",
"amount": 1500,
"status": "pending",
"...": "same fields as Get Refund Status"
}
],
"pagination": { "page": 1, "limit": 20, "total": 1, "pages": 1 }
}
}Refund webhooks
If you've set a webhook URL, Gafiapay sends refund.completed or refund.failed when a refund finishes. Webhooks are signed the same way as payment webhooks; see Payment notifications.
Gafiapay also sends refund.completed when it automatically returns a payment that was made to one of your expired or closed virtual accounts.
{
"event": "refund.completed",
"data": {
"transaction": {
"id": "6710a1c2e4b0f5a3c1d2e3f4",
"orderId": "REF1760500000123A1B2C3D4",
"orderNo": "PP25101500012345",
"amount": 1500,
"currency": "NGN",
"status": "completed",
"originalTransaction": "670f9b11e4b0f5a3c1d2e001",
"reason": "Customer cancelled the order",
"completedAt": "2026-10-15T11:02:41.000Z"
}
},
"timestamp": "1760526161000",
"signature": "hmac_sha256_signature"
}