Use the Pay1st Console to generate the Webhooks and Callbacks for your integrations
Used in Gateway Integration
Overview
Partners will receive webhook notifications when processing refunds.
Refund Webhook Configuration
Partners need to configure a webhook URL for receiving refund notifications. This can be done either in the Pay1st Console or as part of the Refund Request API.
| Name | Description | Type |
|---|---|---|
| Refund Webhook URL | This is used in order to send refund updates to the Partner backend server. | String |
Sample Webhook Payload
{
"refundReference": "C-7MV6T02M8O88UCRCBYW-R",
"transactionReference": "C-CA5CGGMV7QQG1HUY05Z-P",
"partnerReference": "48502830697945",
"partnerRefundReference": "refund-7f3c2a91",
"status": "SUCCESSFUL",
"refundAmount": 50.00,
"currencyCode": "NGN",
"reason": "Customer requested refund",
"createdDate": "2026-03-10 12:05:00"
}Refund Webhook Event List of Fields
Fields with no value are left out of the payload. For example, partnerRefundReference is not sent if you did not provide one in the refund request.
| Name | Type | Description |
|---|---|---|
| refundReference | String | The unique identifier of the refund |
| transactionReference | String | The original Pay1st transaction reference |
| partnerReference | String | The partnerReference of the original payment |
| partnerRefundReference | String | Your refund identifier from the refund request. Left out if you did not send one |
| status | String | The status of the refund: PROCESSING, SUCCESSFUL, FAILED or REJECTED.SUCCESSFUL, FAILED and REJECTED are final. You may receive more than one webhook for the same refund (for example PROCESSING, then SUCCESSFUL); use the latest status. |
| refundAmount | Number | The amount of the refund |
| currencyCode | String | The currency code of the refund |
| reason | String | The reason given in the refund request. Left out if none was given |
| createdDate | String | The date and time the refund was created, in UTC, in the format yyyy-MM-dd HH:mm:ss (e.g. 2026-03-10 12:05:00) |
Verifying the Webhook
Every refund webhook is sent as an HTTP POST with Content-Type: application/json and these headers:
| Header | Description |
|---|---|
X-PARTNER-ID | Your Pay1st partner id |
X-Timestamp | The time the webhook was sent, as Unix epoch milliseconds (e.g. 1789993877496). This is different from the ISO-8601 timestamp you send on your own API requests. |
X-SIGNATURE | The signature of the webhook |
Verify every webhook before you act on it:
- Read the raw request body exactly as received, before any JSON parsing.
- Build the string to verify: the
X-Timestampheader value immediately followed by the raw body (timestamp + body), with nothing in between. - Calculate
HMAC-SHA256using your Signing Key as the key and the string from step 2 as the data, both as UTF-8. This is the same Signing Key and the same calculation as in Gateway - Generating Signatures. - Hex encode the result in lowercase. Compare it with the
X-SIGNATUREheader using a constant-time comparison. - If they do not match, reject the webhook and do not update the refund.
Test Values
| Item | Value |
|---|---|
| Signing Key | hCyO_Flnu6aid-bhFYTYOowkxXRzoZkgzO32rB6Ik8Y |
X-Timestamp | 1789993877496 |
Expected X-SIGNATURE | 225d000691ab2be9f77b16b6645af2035c6798549d9dcf6e35d35eed79eedfba |
Raw body (a single line, no trailing newline):
{"refundReference":"C-7MV6T02M8O88UCRCBYW-R","transactionReference":"C-CA5CGGMV7QQG1HUY05Z-P","partnerReference":"48502830697945","status":"SUCCESSFUL","refundAmount":50.00,"currencyCode":"NGN","reason":"Customer requested refund","createdDate":"2026-03-10 12:05:00"}Code Samples
import hashlib
import hmac
def is_valid_refund_webhook(signing_key: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
expected = hmac.new(
signing_key.encode("utf-8"),
timestamp.encode("utf-8") + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
# Test values from the table above
print(is_valid_refund_webhook(
"hCyO_Flnu6aid-bhFYTYOowkxXRzoZkgzO32rB6Ik8Y",
"1789993877496",
b'{"refundReference":"C-7MV6T02M8O88UCRCBYW-R","transactionReference":"C-CA5CGGMV7QQG1HUY05Z-P","partnerReference":"48502830697945","status":"SUCCESSFUL","refundAmount":50.00,"currencyCode":"NGN","reason":"Customer requested refund","createdDate":"2026-03-10 12:05:00"}',
"225d000691ab2be9f77b16b6645af2035c6798549d9dcf6e35d35eed79eedfba",
)) # Trueconst crypto = require("crypto");
function isValidRefundWebhook(signingKey, timestamp, rawBody, signature) {
const expected = crypto
.createHmac("sha256", signingKey)
.update(timestamp + rawBody, "utf8")
.digest("hex");
return expected.length === signature.length
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
// Test values from the table above
console.log(isValidRefundWebhook(
"hCyO_Flnu6aid-bhFYTYOowkxXRzoZkgzO32rB6Ik8Y",
"1789993877496",
'{"refundReference":"C-7MV6T02M8O88UCRCBYW-R","transactionReference":"C-CA5CGGMV7QQG1HUY05Z-P","partnerReference":"48502830697945","status":"SUCCESSFUL","refundAmount":50.00,"currencyCode":"NGN","reason":"Customer requested refund","createdDate":"2026-03-10 12:05:00"}',
"225d000691ab2be9f77b16b6645af2035c6798549d9dcf6e35d35eed79eedfba"
)); // true<?php
function isValidRefundWebhook(string $signingKey, string $timestamp, string $rawBody, string $signature): bool
{
$expected = hash_hmac('sha256', $timestamp . $rawBody, $signingKey);
return hash_equals($expected, $signature);
}
// In your webhook handler:
// $rawBody = file_get_contents('php://input');
// $timestamp = $_SERVER['HTTP_X_TIMESTAMP'];
// $signature = $_SERVER['HTTP_X_SIGNATURE'];
// Test values from the table above
var_dump(isValidRefundWebhook(
'hCyO_Flnu6aid-bhFYTYOowkxXRzoZkgzO32rB6Ik8Y',
'1789993877496',
'{"refundReference":"C-7MV6T02M8O88UCRCBYW-R","transactionReference":"C-CA5CGGMV7QQG1HUY05Z-P","partnerReference":"48502830697945","status":"SUCCESSFUL","refundAmount":50.00,"currencyCode":"NGN","reason":"Customer requested refund","createdDate":"2026-03-10 12:05:00"}',
'225d000691ab2be9f77b16b6645af2035c6798549d9dcf6e35d35eed79eedfba'
)); // bool(true)Responding to the Webhook
- Respond with any HTTP
2xxstatus code to confirm you have received the webhook. The response body is ignored. - Make your handler idempotent: the same status for the same
refundReferencecan arrive more than once. - If your endpoint was unavailable or you think you missed a webhook, get the current status with Query Refund by Reference or List Refunds.
