Checkout Link

Request
Request to generate a checkout page session URL
Path
POST /checkout/link
Example (Basic)
Basic example to create a one-time payment link with a fixed amount.
{
"type": "CHOOSE_WHAT_TO_PAY",
"item": {
"title": "My product name",
"frequency": ["ONE_TIME"], // [optional] default "ONE_TIME"
"amount": "1200.00" // [optional] preset an amount to pay else an amount field will show for the user to enter
}
}
Example (Donation form)
An example of a donation form with one-time, monthly and annual subscription options and an amount field the user can select.
Flexible checkouts (CHOOSE_WHAT_TO_PAY) use a single item object for the payer-facing offer. customization.confirmation_message appears after successful payment unless you rely on redirects. Customer handling: send customer_id for an existing record, send a customer object to create or prefill, or omit both and optionally supply customer_fields for extra form fields (name, surname, and email always show).
{
"type": "CHOOSE_WHAT_TO_PAY",
// Pass your product / campaign details via item
"item": {
"title": "My product name", // Product title, will appear on invoice
"description": "My product description", // [optional] longer description
"frequency": ["ONE_TIME", "MONTHLY", "ANNUALLY"], // options the payer may choose among
"amount_options": [50, 100, 150, 200, 250], // [optional] preset amounts (max five)
"amount_max": 3000 // [optional] cap when payer enters any amount
},
"customization": {
"button_text": "Donate now", // [optional] primary button label — see Advanced for full options
"confirmation_message": "Thank you for your donation" // [optional] shown after payment; else use redirects
},
"signature": "secret-key-for-payload", // [optional] payload signature when configured
"notification": {
"email": "me@my-email.co.za", // [optional] email notifications on successful payment
"webhook_url": "https://merchant.example/webhooks/payments" // webhook URL for each successful payment
},
// Issue and optionally email an invoice after payment
"invoice": {
"is_generate": true,
"is_send": true // send paid invoice to the customer when true
},
// If neither customer_id nor customer is passed, show a form — these are extra collectable fields
"customer_fields": [
"CONTACT_NUMBER",
"ID_NUMBER",
"COMPANY_NAME",
"COMPANY_REG",
"VAT_NUMBER",
"BILLING_ADDRESS",
"SHIPPING_ADDRESS"
]
}
Example (Products, subscriptions and invoices)
Example with a recurring and one-time product to create a subscription with invoices.
{
"type": "PRODUCTS_AND_SUBSCRIPTIONS",
"customization": {
"button_text": "Pay now" // [optional] button text — default label if omitted depends on checkout
},
"signature": "secret-key-for-payload", // [optional] shared-secret signature for the payload
"customer_id": "cus_abc123...", // existing customer record; alternatively pass full customer object
// Catalog lines (one-off and recurring products may be mixed — see Products reference)
"items": [
{
"product_id": "pro_abc123...",
"qty": 2 // default 1 when omitted
},
{
"product_id": "pro_xyz789..."
},
{
"product_id": "pro_xyz789...",
"price_excl": 300.23 // override the default product amount
}
],
// Invoice for this checkout / subscriptions created from it
"invoice": {
"is_generate": true, // generate invoice for the basket or subscription billing
"is_send": true // email invoice when paid
},
// Redirects after payer finishes or taps cancel
"redirects": {
"success_url": "https://merchant.example/success?myquery=myparam", // may append &signature=
"cancel_url": "https://merchant.example/cancel"
}
}
Example (Advanced )
Advanced example with example with all options
{
"type": "PRODUCTS_AND_SUBSCRIPTIONS",
"transaction_reference": "INV-2026-00042", // [optional] your transaction reference; when omitted the card transaction reference is used
"signature": "secret-key-for-payload", // [optional] shared-secret signature for the payload, see the signature section below
// Inline customer create / prefilled profile (alternative: customer_id)
"customer": {
"reference": "CLN240919000001",
"company_name": null,
"person_name": "John",
"person_surname": "Doe",
"client_type": "RESIDENT_INDIVIDUAL", // [optional]
"id_type": "SOUTH_AFRICAN_ID",
"id_number": "8007014800087",
"email": "johndoe@mail.com",
"contact_number": "+27831234567",
"billing_address": {
"line1": "1 Loop Street",
"line2": "Floor 5",
"city_or_town": "Cape Town",
"province_or_state": "Western Cape",
"postal_or_zip_code": "8001",
"country": "ZA"
},
"shipping": {
"contact_name": "Jane Doe",
"contact_number": "+27820000000",
"address": {
"line1": "1 Loop Street",
"line2": "Floor 5",
"city_or_town": "Cape Town",
"province_or_state": "Western Cape",
"postal_or_zip_code": "8001",
"country": "ZA"
}
},
"metadata": {
"custom_field_1": "custom data 1",
"custom_field_2": "custom data 2"
}
},
// Basket built from catalogue products — discount per line optional
"items": [
{
"product_id": "pro_abc123...",
"qty": 2,
"discount": 100 // discount amount for this item (advanced use)
},
{
"product_id": "pro_xyz789...",
"qty": 3
},
// ...when no product_id is passed a product can be created
{
"name": "Generic Wooden Ball",
"description": "Good For Training And Recreational Purposes",
"code": "PRD-606075",
"unit_label": "each",
"price_excl": "2409.86",
"is_tax": false,
"pricing_type": "RECURRING",
"product_status": "ACTIVE",
"billing_period": "MONTHLY",
"custom_period": null,
"custom_interval": null,
"picture_default": null,
"pictures": {} // {...} media bundle when uploading gallery assets programmatically
}
],
"invoice": {
"is_generate": true,
"is_send": true
},
// Restrict rails shown — omit array to expose every method toggled on for the merchant
"payment": {
"payment_methods": ["CARD", "ZAPPER"]
},
"redirects": {
// Successful redirect URLs may append `signature` when signatures are configured.
"success_url": "https://merchant.example/success?myquery=myparam",
"cancel_url": "https://merchant.example/cancel"
},
"settings": {
"expiry_time": 1440 // minutes until checkout link lapses — omit when the link never expires (default)
},
"metadata": {
"order_id": "ord_98765" // [optional] custom key/value pairs returned on the transactions and webhooks
},
"customization": {
"button_text": "Pay",
"type": "PAGE", // or "EMBED" for iframe-style hosting or in-app payments
"is_display_cancel_button": true,
"is_display_total": true,
"is_display_items": true, // cart line detail list
"brand": {
"primary": "#00DC82",
"secondary": "#CCCCCC"
}
}
}
Example (Embed payments)
Embed payments into your mobile app (React Native, Flutter, Swift or Kotlin) or frameworks like (Next.js, Angular or Nuxt.js)

{
"type": "PRODUCTS_AND_SUBSCRIPTIONS",
"customization": {
"button_text": "Pay now", // [optional] button text — default label if omitted depends on checkout
"type": "EMBED", // show embedded screen that can be displayed in a iFrame or WebView, this will also send message events instead of a redirect
"embed_channel": "APP", // [optional] WEB for an iframe on the web, APP for a WebView in a native app — default APP; applies when type is EMBED
"is_display_cancel_button": false, // hide the cancel button, if true will trigger a 'payment.cancelled' message
"is_display_total": false, // hide the total amount
"is_display_items": false, // hide the checkout items
"brand": {
"primary": "#00DC82", // brand according to your app
"secondary": "#CCCCCC"
}
},
"customer_id": "cus_abc123...", // existing customer record; alternatively pass full customer object
// Catalog lines (one-off and recurring products may be mixed — see Products reference)
"items": [
{
"product_id": "pro_abc123...",
"qty": 2 // default 1 when omitted
},
{
"product_id": "pro_xyz789..."
}
],
// Invoice for this checkout / subscriptions created from it
"invoice": {
"is_generate": true, // generate invoice for the basket or subscription billing
"is_send": true // email invoice when paid
},
}
Using Iframes
When adding an embedded checkout to an iframe, please add the following attributes in: allow="payment *" referrerpolicy="strict-origin-when-cross-origin".
Using Apple Pay
When using Apple Pay, please download this file: apple-developer-merchantid-domain-association and upload to your domain under the following path: https://my.owndomain.co.za/.well-known/apple-developer-merchantid-domain-association. You need to upload a file for each subdomain.
Ensure it is served from the URL and does not automatically download but display's the file like this: https://pay.kwik.co.za/.well-known/apple-developer-merchantid-domain-association.
Then contact support and include your link for us to configure your domain.
Using a Native App
The following message events will be sent to your app via a native bridge; use them to drive the next action:
payment.loadedpayment.cancelledpayment.failedpayment.successfulpayment.pendingpayment.closed
The following data will be sent in the message on payment.successful events to verify the transaction:
{
"source": "kwik-payments",
"event": "payment.successful",
"sessionId": "ses_abc123...",
"transactionPublicId": "Abc123xyz...",
"customerId": "cus_abc123...",
"status": "PAID"
}
The following data will be sent in the message on payment.failed events:
{
"source": "kwik-payments",
"event": "payment.failed",
"sessionId": "ses_abc123...",
"customerId": "cus_abc123...",
"status": "DECLINED",
"message": "Your transaction was declined due to insufficient funds"
}
Below are web and mobile app examples to create a checkout session and verify transactions through your own Node.js back-end:
// server.js
const express = require('express');
const app = express();
app.use(express.json());
const KWIK_API = 'https://api.kwik.co.za/v2';
// Keep credentials server-side only
const KWIK_API_KEY = process.env.KWIK_API_KEY;
const KWIK_API_SECRET = process.env.KWIK_API_SECRET;
const basicAuth = Buffer
.from(`${KWIK_API_KEY}:${KWIK_API_SECRET}`)
.toString('base64');
const headers = {
'Content-Type': 'application/json',
'Authorization': `Basic ${basicAuth}`
};
// TODO: Add your own authentication
// 1. Create a checkout session for the app
app.post('/api/checkout', async (req, res) => {
const response = await fetch(`${KWIK_API}/checkout/link`, {
method: 'POST',
headers,
body: JSON.stringify({
type: 'CHOOSE_WHAT_TO_PAY',
item: {
title: 'My product name',
amount: '1200.00' // in production, derive from your own order data
},
customization: {
type: 'EMBED', // required for in-app payments — sends events instead of redirecting
embed_channel: 'APP', // required to specify how events are returned. APP = Mobile apps, Web = Websites
is_display_cancel_button: true
}
})
});
const { status, result } = await response.json();
if (!status) return res.status(502).json({ error: 'Could not create checkout' });
// The app only needs the URL to load
res.json({ checkoutUrl: result.link_url });
});
// 2. Verify a transaction after the app receives payment.successful
app.get('/api/transactions/:transactionPublicId/verify', async (req, res) => {
const { transactionPublicId } = req.params;
const response = await fetch(
`${KWIK_API}/transactions/record/${transactionPublicId}`,
{ headers }
);
const data = await response.json();
const paid = data.status === true
&& data.transaction?.transaction_status === 'PAID';
res.json({ paid });
});
app.listen(3000);
import React, { useEffect, useState } from 'react';
import { ActivityIndicator } from 'react-native';
import { WebView } from 'react-native-webview';
const BACKEND = 'https://your-server.example';
const KWIK_SCHEME = 'kwikpay://';
export function KwikCheckout({ onComplete, onCancel }) {
const [checkoutUrl, setCheckoutUrl] = useState(null);
// Ask your backend to create the checkout session
useEffect(() => {
(async () => {
const res = await fetch(`${BACKEND}/api/checkout`, { method: 'POST' });
const { checkoutUrl } = await res.json();
setCheckoutUrl(checkoutUrl);
})();
}, []);
const verifyTransaction = async (transactionPublicId) => {
const res = await fetch(
`${BACKEND}/api/transactions/${transactionPublicId}/verify`
);
const { paid } = await res.json();
return paid;
};
const handleKwikEvent = async (evt) => {
// evt example on success:
// { source: 'kwik-payments', event: 'payment.successful',
// sessionId: 'ses_...', transactionPublicId: 'tra_...',
// customerId: 'cus_...', status: 'PAID' }
switch (evt.event) {
case 'payment.loaded':
break; // checkout ready
case 'payment.pending':
break; // e.g. 3DS in progress — keep the WebView open
case 'payment.successful': {
const paid = await verifyTransaction(evt.transactionPublicId);
if (paid) {
onComplete(evt.transactionPublicId);
}
// If not yet PAID (e.g. still settling), poll again or
// rely on your webhook before fulfilling.
break;
}
case 'payment.failed':
break; // checkout shows its own retry UI — keep it open
case 'payment.cancelled':
case 'payment.closed':
onCancel();
break;
}
};
if (!checkoutUrl) return <ActivityIndicator />;
return (
<WebView
source={{ uri: checkoutUrl }}
applicationNameForUserAgent="KwikPay/2.0"
enableApplePay={true}
thirdPartyCookiesEnabled={true}
onShouldStartLoadWithRequest={(request) => {
if (request.url.startsWith(KWIK_SCHEME)) {
const qs = request.url.split('?')[1] ?? '';
handleKwikEvent(Object.fromEntries(new URLSearchParams(qs)));
return false; // cancel — the checkout page stays loaded
}
return true; // allow 3DS, Ozow, and other payment redirects
}}
/>
);
}
//dependencies:
// flutter_inappwebview: ^6.0.0
// http: ^1.2.0
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:http/http.dart' as http;
const backend = 'https://your-server.example';
const kwikScheme = 'kwikpay';
class KwikCheckout extends StatefulWidget {
final void Function(String transactionPublicId) onComplete;
final VoidCallback onCancel;
const KwikCheckout({
super.key,
required this.onComplete,
required this.onCancel,
});
@override
State<KwikCheckout> createState() => _KwikCheckoutState();
}
class _KwikCheckoutState extends State<KwikCheckout> {
String? checkoutUrl;
@override
void initState() {
super.initState();
_createCheckout();
}
// Ask your backend to create the checkout session
Future<void> _createCheckout() async {
final res = await http.post(Uri.parse('$backend/api/checkout'));
final body = jsonDecode(res.body) as Map<String, dynamic>;
setState(() => checkoutUrl = body['checkoutUrl'] as String);
}
Future<bool> _verifyTransaction(String transactionPublicId) async {
final res = await http.get(
Uri.parse('$backend/api/transactions/$transactionPublicId/verify'),
);
final body = jsonDecode(res.body) as Map<String, dynamic>;
return body['paid'] == true;
}
Future<void> _handleKwikEvent(Map<String, String> evt) async {
// evt example on success:
// { source: 'kwik-payments', event: 'payment.successful',
// sessionId: 'ses_...', transactionPublicId: 'tra_...',
// customerId: 'cus_...', status: 'PAID' }
switch (evt['event']) {
case 'payment.loaded':
break; // checkout ready
case 'payment.pending':
break; // e.g. 3DS in progress — keep the WebView open
case 'payment.successful':
final paid = await _verifyTransaction(evt['transactionPublicId']!);
if (paid) {
widget.onComplete(evt['transactionPublicId']!);
}
// If not yet PAID (e.g. still settling), poll again or
// rely on your webhook before fulfilling.
break;
case 'payment.failed':
break; // checkout shows its own retry UI — keep it open
case 'payment.cancelled':
case 'payment.closed':
widget.onCancel();
break;
}
}
@override
Widget build(BuildContext context) {
if (checkoutUrl == null) {
return const Center(child: CircularProgressIndicator());
}
return InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(checkoutUrl!)),
initialSettings: InAppWebViewSettings(
applicationNameForUserAgent: 'KwikPay/2.0',
applePayAPIEnabled: true, // iOS
thirdPartyCookiesEnabled: true, // Android
useShouldOverrideUrlLoading: true, // required for the callback below
),
shouldOverrideUrlLoading: (controller, navigationAction) async {
final uri = navigationAction.request.url;
if (uri != null && uri.scheme == kwikScheme) {
_handleKwikEvent(uri.queryParameters);
return NavigationActionPolicy.CANCEL; // checkout page stays loaded
}
return NavigationActionPolicy.ALLOW; // allow 3DS, Ozow, etc.
},
);
}
}
import UIKit
import WebKit
final class KwikCheckoutViewController: UIViewController, WKNavigationDelegate {
private let backend = URL(string: "https://your-server.example")!
private let kwikScheme = "kwikpay"
var onComplete: ((String) -> Void)?
var onCancel: (() -> Void)?
private var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
config.applicationNameForUserAgent = "KwikPay/2.0"
// Do NOT add WKUserScripts or script message handlers to this
// configuration — script injection disables Apple Pay.
webView = WKWebView(frame: view.bounds, configuration: config)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
webView.navigationDelegate = self
view.addSubview(webView)
createCheckout()
}
// MARK: - Backend calls
/// Ask your backend to create the checkout session
private func createCheckout() {
var request = URLRequest(url: backend.appendingPathComponent("api/checkout"))
request.httpMethod = "POST"
URLSession.shared.dataTask(with: request) { [weak self] data, _, _ in
guard let data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let checkoutUrl = json["checkoutUrl"] as? String,
let url = URL(string: checkoutUrl) else { return }
DispatchQueue.main.async {
self?.webView.load(URLRequest(url: url))
}
}.resume()
}
private func verifyTransaction(_ transactionPublicId: String,
completion: @escaping (Bool) -> Void) {
let url = backend.appendingPathComponent(
"api/transactions/\(transactionPublicId)/verify")
URLSession.shared.dataTask(with: url) { data, _, _ in
let paid = (try? JSONSerialization.jsonObject(with: data ?? Data())
as? [String: Any])?["paid"] as? Bool ?? false
DispatchQueue.main.async { completion(paid) }
}.resume()
}
// MARK: - Event handling
private func handleKwikEvent(_ evt: [String: String]) {
// evt example on success:
// { source: 'kwik-payments', event: 'payment.successful',
// sessionId: 'ses_...', transactionPublicId: 'tra_...',
// customerId: 'cus_...', status: 'PAID' }
switch evt["event"] {
case "payment.loaded":
break // checkout ready
case "payment.pending":
break // e.g. 3DS in progress — keep the WebView open
case "payment.successful":
guard let transactionPublicId = evt["transactionPublicId"] else { return }
verifyTransaction(transactionPublicId) { [weak self] paid in
if paid {
self?.onComplete?(transactionPublicId)
}
// If not yet PAID (e.g. still settling), poll again or
// rely on your webhook before fulfilling.
}
case "payment.failed":
break // checkout shows its own retry UI — keep it open
case "payment.cancelled", "payment.closed":
onCancel?()
default:
break // ignore unknown events for forward compatibility
}
}
// MARK: - WKNavigationDelegate
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url, url.scheme == kwikScheme {
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let evt = Dictionary(
uniqueKeysWithValues: (components?.queryItems ?? [])
.map { ($0.name, $0.value ?? "") }
)
handleKwikEvent(evt)
decisionHandler(.cancel) // checkout page stays loaded
return
}
decisionHandler(.allow) // allow 3DS, Ozow, and other payment redirects
}
}
//dependencies {
// implementation("androidx.webkit:webkit:1.14.0")
//}
import android.annotation.SuppressLint
import android.net.Uri
import android.os.Bundle
import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
class KwikCheckoutActivity : AppCompatActivity() {
private val backend = "https://your-server.example"
private val kwikScheme = "kwikpay"
private val http = OkHttpClient()
private lateinit var webView: WebView
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this)
setContentView(webView)
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
userAgentString = "$userAgentString KwikPay/2.0"
}
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)
// Enable the Payment Request API (Google Pay) when the
// device WebView supports it — androidx.webkit 1.14.0+
if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) {
WebSettingsCompat.setPaymentRequestEnabled(webView.settings, true)
}
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val url = request.url
if (url.scheme == kwikScheme) {
handleKwikEvent(url)
return true // cancel — the checkout page stays loaded
}
return false // allow 3DS, Ozow, and other payment redirects
}
}
createCheckout()
}
// MARK: Backend calls
/** Ask your backend to create the checkout session */
private fun createCheckout() {
lifecycleScope.launch {
val checkoutUrl = withContext(Dispatchers.IO) {
val req = Request.Builder()
.url("$backend/api/checkout")
.post(okhttp3.RequestBody.create(null, ByteArray(0)))
.build()
http.newCall(req).execute().use { res ->
JSONObject(res.body!!.string()).getString("checkoutUrl")
}
}
webView.loadUrl(checkoutUrl)
}
}
private suspend fun verifyTransaction(transactionPublicId: String): Boolean =
withContext(Dispatchers.IO) {
val req = Request.Builder()
.url("$backend/api/transactions/$transactionPublicId/verify")
.build()
http.newCall(req).execute().use { res ->
JSONObject(res.body!!.string()).optBoolean("paid", false)
}
}
// MARK: Event handling
private fun handleKwikEvent(uri: Uri) {
// Event example on success:
// kwikpay://event?source=kwik-payments&event=payment.successful
// &sessionId=ses_...&transactionPublicId=tra_...
// &customerId=cus_...&status=PAID
when (uri.getQueryParameter("event")) {
"payment.loaded" -> Unit // checkout ready
"payment.pending" -> Unit // e.g. 3DS in progress — keep the WebView open
"payment.successful" -> {
val transactionPublicId =
uri.getQueryParameter("transactionPublicId") ?: return
lifecycleScope.launch {
val paid = verifyTransaction(transactionPublicId)
if (paid) {
onComplete(transactionPublicId)
}
// If not yet PAID (e.g. still settling), poll again or
// rely on your webhook before fulfilling.
}
}
"payment.failed" -> Unit // checkout shows its own retry UI — keep it open
"payment.cancelled", "payment.closed" -> onCancel()
else -> Unit // ignore unknown events for forward compatibility
}
}
private fun onComplete(transactionPublicId: String) {
// Payment verified — dismiss the checkout and continue your flow
finish()
}
private fun onCancel() {
finish()
}
}
<div id="checkout-container"></div>
<script>
const KWIK_ORIGIN = 'https://pay.kwik.co.za';
const BACKEND = 'https://your-server.example';
async function startCheckout() {
const { checkoutUrl } = await fetch(`${BACKEND}/api/checkout`, {
method: 'POST',
}).then((r) => r.json());
const iframe = document.createElement('iframe');
iframe.src = checkoutUrl;
iframe.allow = 'payment *'; // required for Apple Pay / Payment Request
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
iframe.style.cssText = 'width:100%;height:560px;border:0';
document.getElementById('checkout-container').replaceChildren(iframe);
}
window.addEventListener('message', async (e) => {
if (e.origin !== KWIK_ORIGIN) return; // trust only the checkout
const evt = e.data || {};
if (evt.source !== 'kwik-payments') return;
switch (evt.event) {
case 'payment.successful': {
const { paid } = await fetch(
`${BACKEND}/api/transactions/${evt.transactionPublicId}/verify`
).then((r) => r.json());
if (paid) {
// fulfil the order / show your own confirmation
}
break;
}
case 'payment.cancelled':
case 'payment.closed':
document.getElementById('checkout-container').replaceChildren();
break;
// payment.loaded / payment.pending / payment.failed:
// no action needed — the checkout handles its own UI
}
});
startCheckout();
</script>
Request Parameters
Fields below appear in the request body examples on this page. Y = required for all requests, C = required or applicable depending on type and whether you pass customer_id vs inline customer.
Field | Required | Type | Description | Example |
|---|---|---|---|---|
| type | Y | ENUM | Checkout mode: CHOOSE_WHAT_TO_PAY (flexible / donation-style) or PRODUCTS_AND_SUBSCRIPTIONS (catalog line items) | CHOOSE_WHAT_TO_PAY |
| item | C | Object | Present when type is CHOOSE_WHAT_TO_PAY | See basic and donation examples |
| item.title | C | String | Product or payment title; shown on the checkout and invoice | My product name |
| item.description | N | String | Optional longer product description | My product description |
| item.frequency | N | Array | Frequencies the payer can choose (e.g. one-time and subscriptions). Defaults to ONE_TIME if omitted | ["ONE_TIME", "MONTHLY", "ANNUALLY"] |
| item.amount | N | String | Preset amount; if omitted the user can enter an amount | 1200.00 |
| item.amount_options | N | Array | Preset amounts the user can pick from, max 5 amounts | [50.00, 100.00, 150.00, 200.00, 250.00] |
| item.amount_max | N | Number | Maximum amount allowed when the user enters an amount | 3000.00 |
| items | C | Array | Present when type is PRODUCTS_AND_SUBSCRIPTIONS; each row either references product_id or defines an inline product (omit product_id), see products | See examples |
| items.product_id | C | String | Catalog product id when referencing an existing product; omit for an inline-defined line item | pro_abc123... |
| items.qty | N | Integer | Quantity for this line item (default 1) | 2 |
| items.discount | N | Number | Discount amount for catalog lines (product_id present), advanced use | 100.00 |
| items.name | C | String | Product name when defining an inline item (no product_id) | Generic Wooden Ball |
| items.description | N | String | Inline item description | Good For Training And Recreational Purposes |
| items.code | C | String | Unique product code; record may be created when unique | PRD-606075 |
| items.unit_label | N | String | Unit label | each |
| items.price_excl | C | String | Price excluding tax | 2409.86 |
| items.is_tax | N | Boolean | Whether tax applies | false |
| items.pricing_type | C | ENUM | e.g. RECURRING or one-time as per your catalog | RECURRING |
| items.product_status | N | ENUM | e.g. ACTIVE | ACTIVE |
| items.billing_period | C | String | When recurring (e.g. MONTHLY) | MONTHLY |
| items.custom_period | N | null | Custom billing period detail if applicable | null |
| items.custom_interval | N | null | Custom interval if applicable | null |
| items.picture_default | N | null | Default image reference | null |
| items.pictures | N | Object | Gallery or image payload | {} |
| transaction_reference | N | String(35) | Your reference for the resulting transaction; appears on statements and reporting. When omitted, the card transaction reference is used | INV-2026-00042 |
| signature | N | String | Optional passphrase signature for verifying the payload | secret-key-for-payload |
| customer_id | C | String | Use an existing customer record instead of customer inline object | cus_abc123... |
| customer_fields | N | Array | Extra fields on the payer form when neither customer_id nor customer is supplied; name, surname and email always show | ["CONTACT_NUMBER", "ID_NUMBER", "COMPANY_NAME", "COMPANY_REG", "VAT_NUMBER", "BILLING_ADDRESS", "SHIPPING_ADDRESS"] |
| customer | C | Object | Create or prefill payer details when not using only customer_id | See advanced example |
| customer.reference | C | String(35) | Your reference for this customer | CLN240919000001 |
| customer.company_name | C | String(64) | Company name when applicable | null |
| customer.person_name | C | String(32) | First name | John |
| customer.person_surname | C | String(32) | Surname | Doe |
| customer.client_type | N | ENUM | RESIDENT_INDIVIDUAL, NON_RESIDENT_INDIVIDUAL, DOMESTIC_COMPANY, FOREIGN_COMPANY, FINANCIAL_INSTITUTION | RESIDENT_INDIVIDUAL |
| customer.id_type | N | ENUM | SOUTH_AFRICAN_ID, PASSPORT_NUMBER, TEMPORARY_RESIDENCE, COMPANY_REGISTRATION_NUMBER | SOUTH_AFRICAN_ID |
| customer.id_number | N | String(13) | Identity or registration number | 8007014800087 |
| customer.email | C | String(128) | Email address | johndoe@mail.com |
| customer.contact_number | C | String(16) | Contact number | +27831234567 |
| customer.billing_address.line1 | N | String | Billing line 1 | 1 Loop Street |
| customer.billing_address.line2 | N | String | Billing line 2 | Floor 5 |
| customer.billing_address.city_or_town | N | String | City or town | Cape Town |
| customer.billing_address.province_or_state | N | String | Province or state | Western Cape |
| customer.billing_address.postal_or_zip_code | N | String | Postal or ZIP code | 8001 |
| customer.billing_address.country | N | String(2) | ISO 3166-1 alpha-2 country code | ZA |
| customer.shipping.contact_name | N | String | Shipping contact name | Jane Doe |
| customer.shipping.contact_number | N | String(16) | Shipping contact number | +27820000000 |
| customer.shipping.address.line1 | N | String | Shipping line 1 | 1 Loop Street |
| customer.shipping.address.line2 | N | String | Shipping line 2 | Floor 5 |
| customer.shipping.address.city_or_town | N | String | Shipping city | Cape Town |
| customer.shipping.address.province_or_state | N | String | Shipping province or state | Western Cape |
| customer.shipping.address.postal_or_zip_code | N | String | Shipping postal code | 8001 |
| customer.shipping.address.country | N | String(2) | ISO 3166-1 alpha-2 | ZA |
| customer.metadata | N | Object | Custom key/value metadata | {"custom_field_1": "custom data 1"} |
| invoice.is_generate | C | Boolean | Whether to generate an invoice for the payment or subscription | true |
| invoice.is_send | N | Boolean | Whether to send the paid invoice to the customer | true |
| notification.email | N | String | Email address to notify on each successful payment | me@my-email.co.za |
| notification.webhook_url | N | String | Webhook URL for each successful payment | https://merchant.example/webhooks/payments |
| redirects.success_url | N | String | Redirect after successful payment; &signature= appended if configured | https://merchant.example/success?myquery=myparam |
| redirects.cancel_url | N | String | Redirect if the payer cancels | https://merchant.example/cancel |
| payment.payment_methods | N | Array | Limit methods shown (CARD, ZAPPER, SNAPSCAN, APPLE_PAY, GOOGLE_PAY, SCAN_TO_PAY, 1VOUCHER, SCODE, MONEY_BADGER) | [CARD, ZAPPER] |
| settings.expiry_time | N | Integer | Link lifetime in minutes; omit so the link does not expire (per advanced example comment) | 1440 |
| metadata | N | Object | Custom key/value metadata returned on the transactions and webhooks | {"order_id": "ord_98765"} |
| customization.button_text | N | String | Primary action button label | Pay |
| customization.confirmation_message | N | String | Message after successful payment (donation example); otherwise use redirects | Thank you for your donation |
| customization.type | N | ENUM | Set to PAGE (default) for a hosted checkout page you can share via a link or open in a new tab — the payer is redirected on completion. Set to EMBED when the checkout runs inside your own product (an iframe on the web, or a WebView in a native app): no redirects are triggered; instead, lifecycle events such as payment.successful are sent to your host page or app. See Embed Payments Example for the native event bridge. | PAGE |
| customization.embed_channel | N | ENUM | Where an EMBED checkout is hosted: WEB (iframe on the web) or APP (WebView in a native app). Default APP. Only applies when customization.type is EMBED | APP |
| customization.is_display_cancel_button | N | Boolean | Show cancel control | true |
| customization.is_display_total | N | Boolean | Show total | true |
| customization.is_display_items | N | Boolean | Show line items | true |
| customization.brand.primary | N | String | Primary hex colour | #00DC82 |
| customization.brand.secondary | N | String | Secondary hex colour | #CCCCCC |
Response Body
{
"status": true,
"result": {
"id": "chk_HVpCeoNys1f22X7QcuWHY",
"session_id": "ses_G-xkVKoxHgEBrY8suKgR3",
"amount": "1200.00",
"currency": "ZAR",
"customer_id": "cus_abc123...",
"link_url": "https://pay.kwik.co.za/checkout/cs_test_a1b2c3",
"expires_at": "2025-09-13T12:30:00Z"
}
}
Response Parameters
Field | Type | Description | Example |
|---|---|---|---|
| status | Boolean | Indicates if the request was successful | true |
| result.id | String | Unique checkout session identifier | chk_HVpCeoNys1f22X7QcuWHY |
| result.session_id | String (32) | Session identifier for the checkout | ses_G-xkVKoxHgEBrY8suKgR3 |
| result.amount | String | Checkout amount | 1200.00 |
| result.currency | String(32) | ISO 4217 currency code | ZAR |
| result.customer_id | String(32) | Associated customer ID | cus_abc123... |
| result.link_url | String | URL to redirect user to complete checkout | https://pay.kwik.co.za/checkout/cs_test_a1b2c3 |
| result.expires_at | String | ISO timestamp when the checkout session expires | 2025-09-13T12:30:00Z |
Signature creation
When creating API keys on the dashboard you can download a passphrase key, use the key in the examples below to generate your signature and send it in the signature parameter:
const crypto = require('crypto')
function canonicalize(value) {
if (value === null || typeof value !== 'object') {
return JSON.stringify(value)
}
if (Array.isArray(value)) {
return `[${value.map(canonicalize).join(',')}]`
}
return `{${Object.keys(value)
.filter((key) => key !== 'signature' && value[key] !== undefined)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`)
.join(',')}}`
}
function generateKwikSignature(payload, passphrase) {
const canonicalPayload = canonicalize(payload)
return crypto
.createHmac('sha256', passphrase)
.update(canonicalPayload)
.digest('hex')
}
// Usage example
const payload = {
type: 'CHOOSE_WHAT_TO_PAY',
item: {
title: 'My product name',
frequency: ['ONE_TIME'], // optional
amount: '1200.00' // optional
}
}
// get this from the dashboard when creating an API key
const passphrase = 'your-secure-passphrase'
const signature = generateKwikSignature(payload, passphrase)
const requestBody = {
...payload,
signature
}
console.log('Canonical payload:', canonicalize(payload))
console.log('Signature:', signature)
console.log('Request body:', requestBody)
<?php
function canonicalize($value) {
if ($value === null) return 'null';
if (is_bool($value)) return $value ? 'true' : 'false';
if (is_int($value) || is_float($value)) return json_encode($value);
if (is_string($value)) return json_encode($value);
if (is_array($value)) {
$isList = array_keys($value) === range(0, count($value) - 1);
if ($isList) {
return '[' . implode(',', array_map('canonicalize', $value)) . ']';
}
ksort($value);
$parts = [];
foreach ($value as $key => $item) {
if ($key === 'signature') continue;
$parts[] = json_encode((string)$key) . ':' . canonicalize($item);
}
return '{' . implode(',', $parts) . '}';
}
return json_encode($value);
}
function generateKwikSignature($payload, $passphrase) {
return hash_hmac('sha256', canonicalize($payload), $passphrase);
}
// Usage
$payload = [
"type" => "CHOOSE_WHAT_TO_PAY",
"item" => [
"title" => "My product name",
"frequency" => ["ONE_TIME"],
"amount" => "1200.00"
]
];
$passphrase = 'your-secure-passphrase';
$signature = generateKwikSignature($payload, $passphrase);
$requestBody = array_merge($payload, [
"signature" => $signature
]);
print_r($requestBody);
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
public class KwikSignature
{
public static string Canonicalize(object value)
{
if (value == null) return "null";
if (value is string || value is bool || value is int || value is long || value is decimal || value is double)
return JsonSerializer.Serialize(value);
if (value is IEnumerable<object> list)
return "[" + string.Join(",", list.Select(Canonicalize)) + "]";
if (value is IDictionary<string, object> dict)
{
var parts = dict
.Where(kvp => kvp.Key != "signature")
.OrderBy(kvp => kvp.Key)
.Select(kvp => JsonSerializer.Serialize(kvp.Key) + ":" + Canonicalize(kvp.Value));
return "{" + string.Join(",", parts) + "}";
}
return JsonSerializer.Serialize(value);
}
public static string GenerateKwikSignature(IDictionary<string, object> payload, string passphrase)
{
var canonical = Canonicalize(payload);
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(passphrase));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
return Convert.ToHexString(hash).ToLowerInvariant();
}
public static void Main()
{
var payload = new Dictionary<string, object>
{
["type"] = "CHOOSE_WHAT_TO_PAY",
["item"] = new Dictionary<string, object>
{
["title"] = "My product name",
["frequency"] = new List<object> { "ONE_TIME" },
["amount"] = "1200.00"
}
};
var passphrase = "your-secure-passphrase";
var signature = GenerateKwikSignature(payload, passphrase);
payload["signature"] = signature;
Console.WriteLine(JsonSerializer.Serialize(payload));
}
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class KwikSignature {
public static String canonicalize(Object value) {
if (value == null) return "null";
if (value instanceof String) return "\"" + value + "\"";
if (value instanceof Boolean || value instanceof Number) return value.toString();
if (value instanceof List<?>) {
List<?> list = (List<?>) value;
List<String> parts = new ArrayList<>();
for (Object item : list) parts.add(canonicalize(item));
return "[" + String.join(",", parts) + "]";
}
if (value instanceof Map<?, ?> map) {
List<String> keys = new ArrayList<>();
for (Object k : map.keySet()) {
if (!"signature".equals(k)) keys.add((String) k);
}
Collections.sort(keys);
List<String> parts = new ArrayList<>();
for (String key : keys) {
parts.add("\"" + key + "\":" + canonicalize(map.get(key)));
}
return "{" + String.join(",", parts) + "}";
}
return "\"" + value.toString() + "\"";
}
public static String generateKwikSignature(Map<String, Object> payload, String passphrase) throws Exception {
String canonical = canonicalize(payload);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(passphrase.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hash) hex.append(String.format("%02x", b));
return hex.toString();
}
public static void main(String[] args) throws Exception {
Map<String, Object> payload = new HashMap<>();
payload.put("type", "CHOOSE_WHAT_TO_PAY");
Map<String, Object> item = new HashMap<>();
item.put("title", "My product name");
item.put("frequency", List.of("ONE_TIME"));
item.put("amount", "1200.00");
payload.put("item", item);
String passphrase = "your-secure-passphrase";
String signature = generateKwikSignature(payload, passphrase);
payload.put("signature", signature);
System.out.println(payload);
}
}
import json
import hmac
import hashlib
def canonicalize(value):
if value is None or not isinstance(value, (dict, list)):
return json.dumps(value, separators=(',', ':'))
if isinstance(value, list):
return '[' + ','.join(canonicalize(v) for v in value) + ']'
return '{' + ','.join(
json.dumps(k) + ':' + canonicalize(value[k])
for k in sorted(value.keys())
if k != 'signature'
) + '}'
def generate_kwik_signature(payload, passphrase):
canonical = canonicalize(payload)
return hmac.new(
passphrase.encode(),
canonical.encode(),
hashlib.sha256
).hexdigest()
# Usage
payload = {
"type": "CHOOSE_WHAT_TO_PAY",
"item": {
"title": "My product name",
"frequency": ["ONE_TIME"],
"amount": "1200.00"
}
}
passphrase = "your-secure-passphrase"
signature = generate_kwik_signature(payload, passphrase)
request_body = {
**payload,
"signature": signature
}
print(request_body)
Webhook
When a checkout session is completed, updated, or expires, a webhook may be delivered to notification.webhook_url when that field was supplied on create. Broader platform webhooks are configured separately if applicable.
Possible event values include checkout.completed, checkout.expired, checkout.failed, and others listed under Webhook events. The subscription object is present only when a recurring collection is created.
Webhook Payload
{
// checkout.completed · checkout.expired · checkout.failed · etc.
"event": "checkout.completed",
"data": [
{
"checkout": {
"id": "chk_HVpCeoNys1f22X7QcuWHY",
"session_id": "ses_G-xkVKoxHgEBrY8suKgR3",
"amount": "1200.00",
"currency": "ZAR",
"customer_id": "cus_abc123...",
"card_id": "crd_SFq2E9LskQimPkf2mRqnV", // populated for card tenders
"transaction_id": "tra_pr6CvR_4pvWwmgQ4y3dtY",
"transaction_status": "ACTIVE",
"metadata": {
"order_id": "ord_98765"
},
"expires_at": "2025-09-13T12:30:00Z",
"completed_at": "2025-09-13T10:15:30Z"
},
// Only present once a recurring collection / mandate results from checkout
"subscription": {
"id": "col_Pw5jImVFZfpd0Lplp35op",
"amount": "1200.00",
"currency": "ZAR",
"subscription_status": "ACTIVE",
"payment_method": "CARD",
"payment_method_id": "pam_ak7lHmJ0a2fuD1q4M6b0q",
"created_at": "2025-09-13T10:15:30Z"
}
}
],
"created_at": "2025-09-13T12:30:00Z"
}
Webhook Payload Parameters
Field | Type | Description | Example |
|---|---|---|---|
| event | String | Type of webhook event that occurred | checkout.completed |
| data | Array | Array containing checkout and payment data | ... |
| data.checkout.id | String | Unique checkout session identifier | chk_HVpCeoNys1f22X7QcuWHY |
| data.checkout.session_id | String | Session identifier for the checkout | ses_G-xkVKoxHgEBrY8suKgR3 |
| data.checkout.amount | String | Checkout amount | 1200.00 |
| data.checkout.currency | String(3) | ISO 4217 currency code | ZAR |
| data.checkout.customer_id | String(32) | Associated customer ID | cus_abc123... |
| data.checkout.card_id | String | Card identifier when payment was made with a saved or newly created card | crd_SFq2E9LskQimPkf2mRqnV |
| data.checkout.transaction_id | String | Transaction identifier | tra_pr6CvR_4pvWwmgQ4y3dtY |
| data.checkout.transaction_status | ENUM | Transaction status, see lookups | ACTIVE |
| data.checkout.metadata | Object | Custom key/value metadata supplied on create | {"order_id": "ord_98765"} |
| data.checkout.expires_at | String | ISO timestamp when checkout session expires | 2025-09-13T12:30:00Z |
| data.checkout.completed_at | String | ISO timestamp when checkout was completed | 2025-09-13T10:15:30Z |
| data.subscription.id | String | Subscription identifier | col_Pw5jImVFZfpd0Lplp35op |
| data.subscription.amount | String | Subscription amount | 1200.00 |
| data.subscription.currency | String(3) | ISO 4217 currency code | ZAR |
| data.subscription.subscription_status | ENUM | Subscription status, see lookups | ACTIVE |
| data.subscription.payment_method | String | Payment method used | CARD |
| data.subscription.payment_method_id | String | Payment method id used | pam_ak7lHmJ0a2fuD1q4M6b0q |
| data.subscription.created_at | String | ISO timestamp when payment was created | 2025-09-13T10:15:30Z |
| created_at | String | ISO timestamp when webhook was created | 2025-09-13T10:15:30Z |
Webhook Events
| Event | Description | Trigger Condition | Data Included |
|---|---|---|---|
checkout.completed | Checkout session was successfully completed with payment | When customer completes payment successfully | Checkout details, payment information, transaction data |
checkout.expired | Checkout session expired without completion | When checkout session reaches expiry time without payment | Checkout details only, no payment data |
checkout.failed | Checkout session failed due to payment failure | When payment processing fails (declined card, insufficient funds, etc.) | Checkout details, failed payment attempt, error information |
checkout.abandoned | Customer abandoned the checkout process | When customer leaves checkout page without completing | Checkout details, abandonment timestamp |
checkout.pending | Payment is pending additional verification | When payment requires manual review or 3DS authentication | Checkout details, pending payment status |
checkout.cancelled | Checkout was cancelled by customer or system | When cancel button is used or system cancels due to fraud | Checkout details, cancellation reason |
Webhook Security
All webhooks are sent with the following headers for verification:
X-Signature: HMAC-SHA256 signature of the payloadX-Timestamp: Unix timestamp of when the webhook was sentUser-Agent:Kwik-Webhooks/1.0
Webhook Response
Your endpoint should respond with a 200 status code to acknowledge receipt. Failed webhooks will be retried up to 3 times with exponential backoff.
Subscriptions
Create and manage recurring card subscriptions on a customer's vaulted cards. Line items come from your product catalog — each product's pricing type and billing interval determine what is billed each cycle and on the first cycle only. Supports trial periods, automatic charging with card cascade, plan changes, pausing, and cancellation — with invoice issuance and webhook notifications.
Checkout Form
Create a secure, customizable checkout form session to capture payments with cards, bank transfers, and other payment methods. Supports 3D Secure authentication, cards storage for recurring billing, invoice issuance, customer creation, and webhook notifications.