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.
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.
Card payments on Payaza can follow one of two flows depending on whether the card is enrolled in 3D Secure (3DS):
Non-3DS flow (simple)
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.messagepostMessage event
listener.
3DS flow (authentication required)
(Optional) Call Check 3DS Availability upfront to determine if the card requires 3DS.
Your server calls Card Charge with the customer and card details.
The response returns do3dsAuth: true along with threeDsUrl, formData, and threeDsHtml.
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.
The customer completes the bank OTP or authentication challenge.
If callback_url was provided: the issuer redirects the customer back to your callback_url via a POST request containing the final payment result.
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.
Card Charge returns one of three response shapes. Always check do3dsAuth first, then statusOk and paymentCompleted.Response 1 — 3DS requiredThis 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.
Response 2 — Payment successfulThis 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.
Response 3 — Payment failedDelivered 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.
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.
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
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
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.
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" } }'
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.
Payaza fires webhook events for only completed card transactions. Webhook notifications are used in conjunction with TSQ API to get the status of card transactionsSample Successful Card Collection payload
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
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.