Refund Webhook Notifications

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.

NameDescriptionType
Refund Webhook URLThis 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.

NameTypeDescription
refundReferenceStringThe unique identifier of the refund
transactionReferenceStringThe original Pay1st transaction reference
partnerReferenceStringThe partnerReference of the original payment
partnerRefundReferenceStringYour refund identifier from the refund request. Left out if you did not send one
statusStringThe 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.
refundAmountNumberThe amount of the refund
currencyCodeStringThe currency code of the refund
reasonStringThe reason given in the refund request. Left out if none was given
createdDateStringThe 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:

HeaderDescription
X-PARTNER-IDYour Pay1st partner id
X-TimestampThe 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-SIGNATUREThe signature of the webhook

Verify every webhook before you act on it:

  1. Read the raw request body exactly as received, before any JSON parsing.
  2. Build the string to verify: the X-Timestamp header value immediately followed by the raw body (timestamp + body), with nothing in between.
  3. Calculate HMAC-SHA256 using 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.
  4. Hex encode the result in lowercase. Compare it with the X-SIGNATURE header using a constant-time comparison.
  5. If they do not match, reject the webhook and do not update the refund.

Test Values

ItemValue
Signing KeyhCyO_Flnu6aid-bhFYTYOowkxXRzoZkgzO32rB6Ik8Y
X-Timestamp1789993877496
Expected X-SIGNATURE225d000691ab2be9f77b16b6645af2035c6798549d9dcf6e35d35eed79eedfba

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",
))  # True
const 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 2xx status code to confirm you have received the webhook. The response body is ignored.
  • Make your handler idempotent: the same status for the same refundReference can 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.