Skip to main content

Key Requirements

Retrieve your API keys from your dashboard by following the steps on our Authentication page. Use your public API key for all card collection requests.
Set the following headers on every request:
{
  "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 for setup instructions.

Supported currencies

Card collections are supported for both NGN and USD.
For NGN cards, the pin parameter is required in the Card Charge request.

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):
  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.
  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.
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.

Step 1 — Card Charge

Initiate a card payment. The response differs based on whether 3DS is required.
transaction_reference has a recommended maximum length of 15 characters**.
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"
    }
  }'

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.
{
  "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.
Kindly note that webhook notifications are only sent for successful card transactions.
{
  "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.
{
  "statusOk": false,
  "message": "Transaction Failed",
  "debugMessage": "BLOCKED",
  "waitForNotification": false,
  "transactionReference": "TXN20240501A",
  "do3dsAuth": false,
  "paymentCompleted": false,
  "amountPaid": 0,
  "valueAmount": 0,
  "acquirer_response_code": ""
}
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.
API reference: 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
{
  "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"
}
Handling the callback (Node.js / Express)
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
{
  "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"
}
postMessage event listener
// 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.
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.
3DS transaction page
<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.
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"
    }
  }'
Sample response
{
  "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"
  }
}
API reference: Check Transaction Status

Step 3 — Initiate Refund

Refund a completed card transaction — either the full amount or a partial amount.
Refunds can only be initiated for transactions with transaction_status: "Completed". Attempting to refund a failed or pending transaction will return an error.
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"
  }'
Kindly note that when performing partial refunds. Ensure that the amounts being refunded doesn’t exceed the original amount that was collected.
Sample response
{
  "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"
  }
}
Save the refund_transaction_reference from the response. This is the reference you will need to check the refund status in Step 4.
API reference: 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.
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"
    }
  }'
Sample response
{
  "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

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.
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>' \
Query parameters
ParameterRequiredDescription
page✓ YesPage number (starts at 1)
size✓ YesNumber of records per page
refund_status✓ YesFilter by status: SUCCESS, Initialized
fromNoStart date — format YYYY-MM-DD
toNoEnd date — format YYYY-MM-DD
currencyNoFilter by currency code (e.g. USD, NGN)
Sample response
{
  "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

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
{
  "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"
}
Always verify the webhook signature before processing the payload. See the Webhooks guide for verification steps.

Error handling

ScenarioWhat to check
statusOk: false on card chargeCheck debugMessage for the reason. Common values: BLOCKED (risk engine), DECLINED (issuer), INVALID_CARD
3DS form not submittingEnsure you are injecting threeDsHtml into the live DOM and not stripping the <script> tag
window.message event not firingOnly 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 requestsConfirm the URL is publicly reachable, accepts POST requests, and does not require authentication.
Refund failsConfirm 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.