> ## Documentation Index
> Fetch the complete documentation index at: https://docs.payaza.africa/llms.txt
> Use this file to discover all available pages before exploring further.

# Card Collections

> This guide explains how to collect payments from customers using debit and credit cards via the Payaza Card Charge API — including 3DS authentication, transaction status checks, and refunds.

## Key Requirements

<Info>
  Retrieve your API keys from your dashboard by following the steps on our
  [Authentication](/guides/authentication) page. Use your **public API key** for
  all card collection requests.
</Info>

Set the following headers on every request:

```json theme={null}
{
  "Authorization": "Payaza <Your public API key encoded in base 64>"
}
```

Make sure your webhook URL is saved on the dashboard and configured to accept `POST` requests. See our [Webhooks guide](/guides/webhooks) for setup instructions.

***

## Supported currencies

Card collections are supported for both NGN and USD.

<Note>
  For **NGN cards**, the `pin` parameter is required in the Card Charge request.
</Note>

***

## How card collections work

Card payments on Payaza can follow one of two flows depending on whether the card is enrolled in 3D Secure (3DS):

<AccordionGroup>
  <Accordion title="Non-3DS flow (simple)">
    1. Your server calls **Card Charge** with the customer and card details. 2.
       The response returns `do3dsAuth: false` and `paymentCompleted: true`. 3. If a
       `callback_url` was provided, the customer is redirected to it with the final
       payment result in the POST body. 4. If no `callback_url` was provided, handle
       the result in the same page via the `window.message` `postMessage` event
       listener.
  </Accordion>

  <Accordion title="3DS flow (authentication required)">
    1. *(Optional)* Call **Check 3DS Availability** upfront to determine if the card requires 3DS.
    2. Your server calls **Card Charge** with the customer and card details.
    3. The response returns `do3dsAuth: true` along with `threeDsUrl`, `formData`, and `threeDsHtml`.
    4. Your frontend injects the `threeDsHtml` into the DOM — this auto-submits a form inside an iframe that redirects the customer to the card issuer's 3DS challenge page.
    5. The customer completes the bank OTP or authentication challenge.
    6. **If `callback_url` was provided:** the issuer redirects the customer back to your `callback_url` via a `POST` request containing the final payment result.
    7. **If no `callback_url` was provided:** the result is posted back to your page via `window.postMessage` and captured by a `window.addEventListener("message", ...)` listener on the parent page.

    <Warning>
      The `threeDsHtml` field contains a self-submitting HTML form that must be rendered directly in the DOM — do not strip the `<script>` tag or render it in a sandboxed context. Keep the iframe on the same page as your `message` event listener so the notification can post back correctly.
    </Warning>
  </Accordion>
</AccordionGroup>

***

## Step 1 — Card Charge

Initiate a card payment. The response differs based on whether 3DS is required.

<Note>
  `transaction_reference` has a recommended **maximum** length of 15
  characters\*\*.
</Note>

<CodeGroup>
  ```bash cURL — Non-NGN (no PIN) theme={null}
  curl --request POST \
    --url https://api.payaza.africa/live/card/card_charge/ \
    --header 'Authorization: Payaza <Your public API key encoded in base 64>' \
    --header 'Content-Type: application/json' \
    --data '{
      "service_payload": {
        "first_name": "John",
        "last_name": "Doe",
        "email_address": "johndoe@example.com",
        "phone_number": "0939344401",
        "amount": 24.00,
        "transaction_reference": "TXN20240501A",
        "currency": "USD",
        "description": "Payment for Order #1234",
        "card": {
          "expiryMonth": "10",
          "expiryYear": "26",
          "securityCode": "686",
          "cardNumber": "4865550017193640"
        },
        "callback_url": "https://yoursite.com/payment/callback"
      }
    }'
  ```

  ```bash cURL — NGN (with PIN) theme={null}
  curl --request POST \
    --url https://api.payaza.africa/live/card/card_charge/ \
    --header 'Authorization: Payaza <YOUR_PUBLIC_KEY>' \
    --header 'Content-Type: application/json' \
    --data '{
      "service_payload": {
        "first_name": "John",
        "last_name": "Doe",
        "email_address": "johndoe@example.com",
        "phone_number": "08012345678",
        "amount": 5000,
        "transaction_reference": "TXN20240501B",
        "currency": "NGN",
        "description": "Payment for Order #5678",
        "card": {
          "expiryMonth": "07",
          "expiryYear": "27",
          "securityCode": "123",
          "cardNumber": "4187451844054629",
          "pin": "1234"
        },
        "callback_url": "https://yoursite.com/payment/callback"
      }
    }'
  ```
</CodeGroup>

### Response types

Card Charge returns one of three response shapes. Always check `do3dsAuth` first, then `statusOk` and `paymentCompleted`.

**Response 1 — 3DS required**

This is returned immediately from the Card Charge endpoint when the card requires 3DS authentication. The payment is **not yet complete** — you must render the `threeDsHtml` to proceed.

```json theme={null}
{
  "statusOk": true,
  "message": "Authentication Required",
  "debugMessage": "3DS Authentication Required",
  "waitForNotification": false,
  "do3dsAuth": true,
  "threeDsUrl": "https://secure-acs2ui.example.com/creq/...",
  "formData": "eyJ0aHJlZURTU2VydmVyVHJhbnNJRCI6...",
  "threeDsHtml": "<div id='threedsChallengeRedirect'>...</div>",
  "paymentCompleted": false,
  "amountPaid": 0,
  "valueAmount": 0
}
```

**Response 2 — Payment successful**

This is the final success response. It is delivered in one of two ways depending on your integration:

* **With `callback_url`:** POSTed to your `callback_url` after 3DS completes, or returned directly for non-3DS cards. Your server receives this and redirects/updates the customer accordingly.
* **Without `callback_url`:** Posted to the parent page via `window.postMessage` and captured by your `message` event listener.

<Note>
  Kindly note that webhook notifications are only sent for successful card
  transactions.
</Note>

```json theme={null}
{
  "statusOk": true,
  "message": "Approved",
  "debugMessage": "Transaction Successful",
  "description": "Test for 3DS",
  "descriptor": " ",
  "waitForNotification": true,
  "transactionReference": "P1KRDXCD5630",
  "customerReference": "P1KRDXCD5630",
  "do3dsAuth": false,
  "paymentCompleted": true,
  "amountPaid": 24,
  "valueAmount": 23.664,
  "payer_name": "John Doe",
  "source_bank_name": "VISA",
  "payment_date": "2024-03-11+19:49:43.575475932",
  "created_at": "2024-03-11+19:49:43.575475932",
  "rrn": "519219078440",
  "risk": {},
  "acquirer_response_code": "00"
}
```

**Response 3 — Payment failed**

Delivered the same way as a success response — either POSTed to `callback_url` or via `postMessage`. Always check `statusOk: false` and read `debugMessage` for the failure reason.

```json theme={null}
{
  "statusOk": false,
  "message": "Transaction Failed",
  "debugMessage": "BLOCKED",
  "waitForNotification": false,
  "transactionReference": "TXN20240501A",
  "do3dsAuth": false,
  "paymentCompleted": false,
  "amountPaid": 0,
  "valueAmount": 0,
  "acquirer_response_code": ""
}
```

<Warning>
  When `waitForNotification: true` appears in a success response, it means the
  payment is confirmed but a final webhook notification will also follow. Never
  fulfil an order based on the initial Card Charge response alone — always wait
  for the final result via `callback_url`, `postMessage`, or the webhook before
  completing business logic.
</Warning>

**API reference:** [Card Charge](/api-reference/card-collection/card-charge)

***

## Handling the payment result

After a card charge, the final payment result is delivered differently depending on whether you included a `callback_url` in the request.

### Path A — With `callback_url`

When a `callback_url` is provided, Payaza POSTs the final payment result to that URL after the 3DS challenge completes (or immediately for non-3DS cards). The customer is redirected to your `callback_url` page.

Your `callback_url` endpoint must accept `POST` requests. Parse the body for `statusOk` and `paymentCompleted` to determine the outcome and complete your business logic (e.g. update the order, redirect the customer to a success or failure page).

**Sample POST body received at `callback_url`**

```json theme={null}
{
  "statusOk": true,
  "message": "Approved",
  "debugMessage": "Transaction Successful",
  "description": "Test for 3DS",
  "descriptor": " ",
  "waitForNotification": true,
  "transactionReference": "P1KRDXCD5630",
  "customerReference": "P1KRDXCD5630",
  "do3dsAuth": false,
  "paymentCompleted": true,
  "amountPaid": 24,
  "valueAmount": 23.664,
  "payer_name": "John Doe",
  "source_bank_name": "VISA",
  "payment_date": "2024-03-11+19:49:43.575475932",
  "created_at": "2024-03-11+19:49:43.575475932",
  "rrn": "519219078440",
  "risk": {},
  "acquirer_response_code": "00"
}
```

```javascript Handling the callback (Node.js / Express) theme={null}
app.post("/payment/callback", (req, res) => {
  const result = req.body;

  if (result.statusOk === true && result.paymentCompleted === true) {
    // Payment confirmed — complete your business logic here
    console.log("Payment successful:", result.transactionReference);
    res.redirect("/order/success?ref=" + result.transactionReference);
  } else {
    // Payment failed
    console.error("Payment failed:", result.debugMessage);
    res.redirect("/order/failed?reason=" + result.debugMessage);
  }
});
```

***

### Path B — Without `callback_url` (postMessage)

When no `callback_url` is included in the Card Charge request, the final payment result is posted back to the parent page from the 3DS iframe using the browser's `window.postMessage` API. Your page must have a `message` event listener in place before the charge is initiated.

**Sample `postMessage` payload received in the browser**

```json theme={null}
{
  "statusOk": true,
  "message": "Approved",
  "debugMessage": "Transaction Successful",
  "description": "test payment",
  "waitForNotification": true,
  "transactionReference": "PL-1KBPSCJCR1051204",
  "customerReference": "PL-1KBPSCJCR1051204",
  "do3dsAuth": false,
  "paymentCompleted": true,
  "amountPaid": 9,
  "valueAmount": 8.874,
  "payer_name": "Loss bell",
  "source_bank_name": "MASTERCARD",
  "payment_date": "2026-04-14 13:20:59.564960693",
  "created_at": "2026-04-14 13:20:59.564960693",
  "rrn": "610413112904",
  "risk": {},
  "acquirer_response_code": "00"
}
```

```javascript postMessage event listener theme={null}
// Set up the listener BEFORE initiating the card charge
window.addEventListener("message", (event) => {
  try {
    const response = JSON.parse(event.data);
    console.log("Payment Notification", response);

    if (response.statusOk !== undefined) {
      if (response.statusOk === true && response.paymentCompleted === true) {
        // Payment successful — complete your business logic
        console.log("Payment successful:", response.transactionReference);
        showSuccessUI(response);
      } else {
        // Payment failed
        console.log("Payment failed:", response.debugMessage);
        showFailureUI(response);
      }
    }
  } catch (error) {
    console.log("Error parsing JSON", error);
  }
});
```

***

### 3DS challenge HTML document

This is the complete HTML document used exclusively for processing 3DS transactions. It handles the full charge request, renders the authentication challenge, and listens for the payment result — all in a single page.

Use this as your reference implementation when building a 3DS-enabled checkout. Replace the card details and API key with your own values.

<Note>
  This document does not include a `callback_url` in the charge request. The
  final payment result is therefore delivered via the internal
  `window.postMessage` listener at the bottom of the script. If you add a
  `callback_url`, the result will be POSTed to that URL instead — see **Path A**
  above.
</Note>

```html 3DS transaction page theme={null}
<body>
  <div
    id="threedsChallengeRedirect"
    xmlns="http://www.w3.org/1999/html"
    style="height: 100vh"
  >
    <form
      id="threedsChallengeRedirectForm"
      method="POST"
      action=""
      target="challengeFrame"
    >
      <input type="hidden" name="creq" id="creq" value="" />
    </form>
    <iframe
      id="challengeFrame"
      name="challengeFrame"
      width="100%"
      height="100%"
    ></iframe>
  </div>

  <script>
    // ─── Card Details ────────────────────────────────────────────────────────────
    const cardNumber = "4187451844054629"; // Card number
    const expiryMonth = "07"; // Card expiry month
    const expiryYear = "32"; // Card expiry year
    const securityCode = "100"; // Card CVV / security code
    const pin = "1000"; // Card PIN — required for NGN cards only

    // ─── Build Request ───────────────────────────────────────────────────────────
    var myHeaders = new Headers();
    myHeaders.append("Authorization", "Payaza <YOUR_PUBLIC_KEY>");
    myHeaders.append("Content-Type", "application/json");

    var raw = JSON.stringify({
      service_type: "Account",
      service_payload: {
        first_name: "John",
        last_name: "Doe",
        email_address: "johndoe@email.com",
        phone_number: "090121980906",
        amount: 11,
        // Generate a unique reference per transaction — max 15 characters
        transaction_reference:
          "PL-1KBPSCJCR" + Math.floor(Math.random() * 10000000 + 1),
        currency: "NGN",
        description: "Test for 3DS",
        card: {
          expiryMonth: expiryMonth,
          expiryYear: expiryYear,
          securityCode: securityCode,
          cardNumber: cardNumber,
          pin: pin, // Required for NGN cards only
        },
        // Add "callback_url": "https://yoursite.com/callback" here
        // if you want the result POSTed to your server instead of
        // handled via the postMessage listener below
      },
    });

    var requestOptions = {
      method: "POST",
      headers: myHeaders,
      body: raw,
      redirect: "follow",
    };

    // ─── Initiate the Card Charge ─────────────────────────────────────────────────
    fetch("https://api.payaza.africa/live/card/card_charge/", requestOptions)
      .then((response) => response.text())
      .then((result) => {
        result = JSON.parse(result);

        if (result.statusOk) {
          // 3DS required — inject formData into the hidden input,
          // set the form action to threeDsUrl, and submit into the iframe.
          // The iframe loads the card issuer's authentication challenge.
          const creq = document.getElementById("creq");
          creq.value = result.formData;

          const form = document.getElementById("threedsChallengeRedirectForm");
          form.setAttribute("action", result.threeDsUrl);
          form.submit();
        } else {
          // Card Charge failed before 3DS could begin (e.g. invalid card,
          // blocked by risk engine). Show the error to the user.
          console.log("Error found", result.debugMessage);
          alert("Payment Failed: " + result.debugMessage);
        }
      })
      .catch((error) => {
        // Network or unexpected error
        console.log("Error", error);
        alert("Exception Error: " + error.message);
      });

    // ─── Internal Payment Notification (postMessage) ─────────────────────────────
    // This listener fires when the 3DS challenge completes and the result is
    // posted back to this page from the iframe. It is only used when no
    // callback_url is included in the request above.
    window.addEventListener("message", (event) => {
      console.log(
        "::::::::::::::::::MESSAGE EVENT GOT BACK FROM BACK-END::::::::::::::::::::::",
      );
      try {
        const response = JSON.parse(event.data);
        console.log("Payment Notification", response);

        if (response.statusOk !== undefined) {
          if (
            response.statusOk === true &&
            response.paymentCompleted === true
          ) {
            // Payment successful — run your business logic here
            // e.g. update the order, redirect to confirmation page
            alert("Payment Successful");
          } else {
            // Payment failed — notify the customer
            alert("Payment Failed");
          }
        }
      } catch (error) {
        console.log("Error from Parsing JSON", error);
      }
    });
  </script>
</body>
```

**Key points about this document:**

* The iframe (`challengeFrame`) renders the card issuer's 3DS authentication page — the customer sees their bank's OTP or biometric prompt inside it.
* `result.formData` maps to the `creq` (challenge request) value that the ACS (Access Control Server) expects. Do not modify it.
* `result.threeDsUrl` is the ACS URL generated by Payaza for that specific transaction. It changes with every charge.
* Replace `alert(...)` with your actual UI logic — redirect the customer, update the order status, or render a success/failure component.

***

## Step 2 — Check Transaction Status

Retrieve the final status of a card transaction by its `transaction_reference`. Use this as a server-side fallback when a webhook is not received within your expected timeout.

<CodeGroup>
  ```bash cURL theme={null}
  curl --request POST \
    --url https://api.payaza.africa/live/card/card_charge/transaction_status \
    --header 'Authorization: Payaza <Your public API key encoded in base 64>' \
    --header 'Content-Type: application/json' \
    --data '{
      "service_payload": {
        "transaction_reference": "TXN20240501A"
      }
    }'
  ```
</CodeGroup>

**Sample response**

<CodeGroup>
  ```json Successful collection theme={null}
  {
    "response_code": 200,
    "response_message": "Transaction data found",
    "response_content": {
      "transaction_reference": "3RDASDDFERF",
      "transaction_amount": 20.28,
      "transaction_fee": 0.28,
      "transaction_amount_payable": 20.28,
      "transaction_status": "Completed",
      "payer_name": "John Doe",
      "source_bank_name": "MASTERCARD",
      "payment_date": "2025-10-01 15:02:22.268",
      "created_at": "2025-10-01 15:02:22.268",
      "debug_message": "Payment Approved"
    }
  }
  ```

  ```json Failed collection theme={null}
  {
    "response_code": 200,
    "response_message": "Transaction data found",
    "response_content": {
      "transaction_reference": "FLADJCR5ASDR56",
      "transaction_amount": 10.0,
      "transaction_fee": 0.14,
      "transaction_amount_payable": 10.0,
      "transaction_status": "Failed",
      "payment_date": "2026-03-24 11:46:38.352841",
      "created_at": "2026-03-24 11:46:38.352843",
      "debug_message": "3DS Authentication Failed"
    }
  }
  ```
</CodeGroup>

**API reference:** [Check Transaction Status](/api-reference/card-collection/check-transaction-status)

***

## Step 3 — Initiate Refund

Refund a completed card transaction — either the full amount or a partial amount.

<Note>
  Refunds can only be initiated for transactions with `transaction_status:
      "Completed"`. Attempting to refund a failed or pending transaction will return
  an error.
</Note>

<CodeGroup>
  ```bash cURL — Full refund theme={null}
  curl --request POST \
    --url https://api.payaza.africa/live/refund-chargeback/refund/merchant/api/refund \
    --header 'Authorization: Payaza <Your public API key encoded in base 64>' \
    --header 'Content-Type: application/json' \
    --data '{
      "transaction_reference": "TXN20240501A",
      "refund_amount": 24.00,
      "refund_reason": "Customer request"
    }'
  ```

  ```bash cURL — Partial refund theme={null}
  curl --request POST \
    --url https://api.payaza.africa/live/refund-chargeback/refund/merchant/api/refund \
    --header 'Authorization: Payaza <Your public API key encoded in base 64>' \
    --header 'Content-Type: application/json' \
    --data '{
      "transaction_reference": "TXN20240501A",
      "refund_amount": 10.00,
      "refund_reason": "Partial cancellation"
    }'
  ```
</CodeGroup>

<Warning>
  Kindly note that when performing **partial refunds**. Ensure that the amounts
  being refunded doesn't exceed the original amount that was collected.{" "}
</Warning>

**Sample response**

```json theme={null}
{
  "message": "Operation Completed",
  "data": {
    "message": "Refund Processed Successfully",
    "refund_transaction_reference": "RF20240402-Z81IZX5OPCG1NGN",
    "payment_transaction_reference": "P-C-20240402-E2KDJ9N4H0",
    "status": "SUCCESS",
    "original_transaction_amount": 15.21,
    "refunded_amount": 15.21,
    "successful": true,
    "rrn": "615098123313"
  }
}
```

<Warning>
  Save the `refund_transaction_reference` from the response. This is the
  reference you will need to check the refund status in Step 4.
</Warning>

**API reference:** [Initiate Refund](/api-reference/refunds-and-chargebacks/initiate-refund)

***

## Step 4 — Check Refund Status

Verify the current status of a refund using the `refund_transaction_reference` returned when the refund was initiated.

<CodeGroup>
  ```bash cURL theme={null}
  curl --request POST \
    --url https://api.payaza.africa/live/card/card_charge/refund_status \
    --header 'Authorization: Payaza <Your public API key encoded in base 64>' \
    --header 'Content-Type: application/json' \
    --data '{
      "service_payload": {
        "refund_transaction_reference": "RF20240501-LDUUSD"
      }
    }'
  ```
</CodeGroup>

**Sample response**

```json theme={null}
{
  "transactionReference": "P-C-20251212-RWYJHCLSXE",
  "refundReference": "RF20251212-KHS0ZOVDOXVFNGN",
  "refundStatus": "SUCCESS",
  "statusReason": "Refund processed successfully",
  "refundDate": "2025-12-12T09:16:15.983184",
  "refundedAmount": 21.3,
  "parentTransactionAmount": 21.3,
  "message": "Request Completed",
  "statusOk": true
}
```

**API reference:** [Check Refund Status](/api-reference/card-collection/check-refund-status)

***

## Step 5 — Fetch Refund History

Retrieve a paginated list of all refund transactions for your account. Results can be filtered by date range, currency, and refund status.

<CodeGroup>
  ```bash cURL — All refunds (paginated) theme={null}
  curl --request GET \
    --url 'https://api.payaza.africa/live/refund-chargeback/refund/merchant/api/refund_history?page=1&size=10&refund_status=SUCCESS' \
    --header 'Authorization: Payaza <YOUR_PUBLIC_KEY>' \
  ```

  ```bash cURL — Filtered by date range and currency theme={null}
  curl --request GET \
    --url 'https://api.payaza.africa/live/refund-chargeback/refund/merchant/api/refund_history?from=2024-05-01&to=2024-05-31&currency=USD&page=1&size=20&refund_status=SUCCESS' \
    --header 'Authorization: Payaza <YOUR_PUBLIC_KEY>' \
  ```
</CodeGroup>

**Query parameters**

| Parameter       | Required | Description                                 |
| --------------- | -------- | ------------------------------------------- |
| `page`          | ✓ Yes    | Page number (starts at 1)                   |
| `size`          | ✓ Yes    | Number of records per page                  |
| `refund_status` | ✓ Yes    | Filter by status: `SUCCESS`, `Initialized`  |
| `from`          | No       | Start date — format `YYYY-MM-DD`            |
| `to`            | No       | End date — format `YYYY-MM-DD`              |
| `currency`      | No       | Filter by currency code (e.g. `USD`, `NGN`) |

**Sample response**

```json theme={null}
{
  "status": "success",
  "message": "Refund History Success.",
  "data": {
    "content": [
      {
        "id": 100805,
        "refund_amount": 20.28,
        "transaction_amount": 20.28,
        "transaction_fee": 0.28,
        "refund_type": "Full Refund",
        "transaction_reference": "RF20240102-28677JHTQY4NNGN",
        "mpgs_refund_type": "REFUNDED",
        "mpgs_refund_status": "SUCCESS",
        "refund_order_id": "P-C-20240102-ZYJKOF1P2A",
        "refund_transaction_id": "RF20240102-28677JHTQY4NNGN",
        "start_at": "2026-01-02 07:49:59.264340",
        "end_at": "2026-01-02 07:50:00.583762",
        "transaction": {
          "id": 31387402,
          "description": "Card Payment Collection for Payment Link",
          "ended_at": "2024-01-02 07:47:07.565000",
          "fee_amount": 0.28,
          "rrn": "600209253283",
          "collection_channel": "Card",
          "started_at": "2024-01-02 07:47:07.565000",
          "status_reason": "Payment Approved",
          "transaction_amount": 20.28,
          "transaction_reference": "P-C-20240102-ZYJKOF1P2A",
          "transaction_status": "Completed",
          "merchant_reference": "testpayment",
          "card_pan": "534413xxxxxx9076",
          "customer": {
            "id": 56847732,
            "email_address": "farcome@gmail.com",
            "first_name": "far",
            "last_name": "Come",
            "mobile_number": "081093190831"
          },
          "currency": {
            "id": 1,
            "name": "Naira",
            "code": "NGN",
            "html_code": "&#8358;",
            "iso_code": "NGN",
            "unicode": "₦",
            "active": true
          }
        },
        "status_reason": "Refund processed successfully",
        "refund_reason": "Refund",
        "merchant_name": "Acme Corp",
        "refund_initiator": "Acme Corp"
      }
    ],
    "total": 1,
    "first": true,
    "last": false,
    "pageable": {
      "page": 1,
      "size": 10
    }
  }
}
```

**API reference:** [Fetch Refund History](/api-reference/refunds-and-chargebacks/fetch-refund-history)

***

## Webhooks

Payaza fires webhook events for only completed card transactions. Webhook notifications are used in conjunction with TSQ API to get the status of card transactions

**Sample Successful Card Collection payload**

```json theme={null}
{
  "transaction_reference": "P-C-20240602-E2KDJ0N1H0",
  "transaction_status": "Funds Received",
  "transaction_fee": 0.21,
  "amount_received": 15.21,
  "initiated_date": "2024-06-02 09:29:41",
  "current_status_date": "2024-06-02 09:32:14",
  "received_from": {
    "account_name": null,
    "account_number": null,
    "bank_name": "MASTERCARD"
  },
  "merchant_reference": "A235D7194738",
  "status": "Completed",
  "session_id": "615339290313",
  "channel": "Card",
  "branch": false,
  "currency_code": "NGN",
  "payaza_account_reference": "",
  "narration": "",
  "business_fk": 100,
  "customer": {
    "email_address": "johndoe@gmail.com",
    "first_name": "John",
    "last_name": "Doe",
    "mobile_number": "08113140331"
  },
  "request_amount": 15.21,
  "amount_validation": "EXACT"
}
```

<Note>
  Always verify the webhook signature before processing the payload. See the
  [Webhooks guide](/guides/webhooks) for verification steps.
</Note>

***

## Error handling

| Scenario                                   | What to check                                                                                                                                                             |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `statusOk: false` on card charge           | Check `debugMessage` for the reason. Common values: `BLOCKED` (risk engine), `DECLINED` (issuer), `INVALID_CARD`                                                          |
| 3DS form not submitting                    | Ensure you are injecting `threeDsHtml` into the live DOM and not stripping the `<script>` tag                                                                             |
| `window.message` event not firing          | Only used when `callback_url` is absent. Confirm the iframe and form are rendered in the same page context as the listener. Check browser console for cross-origin errors |
| `callback_url` not receiving POST requests | Confirm the URL is publicly reachable, accepts `POST` requests, and does not require authentication.                                                                      |
| Refund fails                               | Confirm the original transaction `transaction_status` is `"Completed"` and the `refund_amount` does not exceed the original `amount_received`                             |

***

## Developer notes

* Always generate a unique `transaction_reference` per charge. The recommended maximum length is **15 characters**.
* For NGN card collections, the card `pin` field is required. For all other currencies, omit `pin`.
* **Choosing your result delivery method:** Include `callback_url` in the request if you want the customer redirected to a page you control after payment. Omit it if you are building a single-page checkout and want to handle the result in the browser via `postMessage`.
* When `do3dsAuth: true`, the payment is not yet complete — render the 3DS challenge before expecting any result. The final outcome arrives only after the customer completes the bank authentication.
* When `waitForNotification: true` appears in a success response, it signals that a webhook notification will also follow. Fulfil the order only after receiving the final result — via `callback_url`, `postMessage`, or a webhook — not based on the initial Card Charge response alone.
* Save `refund_transaction_reference` from every Initiate Refund response — it is the only way to check refund status later.
* The `valueAmount` in a successful response is the settlement amount after fees, and may differ from `amountPaid`. Use `amountPaid` for display to customers.
* Use Fetch Refund History with date range filters for reconciliation — it includes the original transaction details nested inside each refund record.
