Card Vault

Request
Request to generate a card vault page session URL where a payer can securely store a card, or manage their stored cards. Only cards are supported on this page — no other payment methods are shown.
Every newly stored card is verified with a 3D Secure authenticated transaction. If item.amount is supplied it is charged as a one-time fee when the card is stored; otherwise a default R2.00 verification amount is charged (an amount is required for 3DS authentication).
Path
POST /card-vault/{customer_id}/link
| Path Parameter | Type | Description | Example |
|---|---|---|---|
| customer_id | String(32) | The customer to store or manage cards for. The customer must exist — see customers | cus_abc123... |
Example (Basic)
Basic example to create a link where the payer stores a card. The default R2.00 verification amount is charged for 3DS.
{
"type": "STORE_CARD",
"item": {
"title": "Save your card" // shown on the vault page and payer statement
}
}
Example (One-time fee)
Charge a one-time fee when the card is stored, instead of the default R2.00 verification amount.
An invoice can be generated for the fee charge. By default the invoice shows a single line from item; pass an items array to populate the invoice with catalog line items instead — see Checkout Link for the same pattern.
{
"type": "STORE_CARD",
"transaction_reference": "INV-2026-00042", // [optional] your reference for the fee charge; appears on statements and reporting
"item": {
"title": "Card activation fee", // shown on the vault page and payer statement
"description": "A once-off activation fee is charged when your card is saved", // [optional]
"amount": "50.00" // one-time fee charged with the 3DS verification; omit to default to "2.00"
},
// [optional] catalog invoice lines — overrides the default single line from item
"items": [
{
"product_id": "pro_abc123...",
"qty": 1 // default 1 when omitted
},
{
"product_id": "pro_xyz789...",
"price_excl": 300.23 // override the default product amount
}
],
// Issue and optionally email an invoice for the fee charge
"invoice": {
"is_generate": true,
"is_send": true // send paid invoice to the customer when true
},
"notification": {
"email": "me@my-email.co.za", // [optional] email notification when a card is stored
"webhook_url": "https://merchant.example/webhooks/cards" // webhook on card vault events
},
"redirects": {
"success_url": "https://merchant.example/card-saved?myquery=myparam", // may append &signature=
"cancel_url": "https://merchant.example/cancel"
}
}
Example (Manage cards)
Create a link where the payer can view, add, remove, set the default, and reorder their stored cards. Adding a card runs the same 3DS verification flow as STORE_CARD.
{
"type": "MANAGE_CARDS",
"item": {
"title": "Manage your cards"
},
"manage": {
"is_allow_add": true, // [optional] default true — payer can add a new card (runs the R2.00 / fee 3DS flow)
"is_allow_remove": true, // [optional] default true — removing the default card promotes the next card by priority
"is_allow_set_default": true, // [optional] default true — payer can choose their default card and reorder fallbacks
"max_cards": 3, // [optional] cap on stored cards for this customer; the link shows an error state when the customer is at the limit — omit for no limit
"is_duplicate_check": true // [optional] default true — rejects storing a card whose PAN fingerprint already exists on this customer
},
"notification": {
"webhook_url": "https://merchant.example/webhooks/cards" // card.stored · card.removed · card.default_changed · card.reordered
},
"redirects": {
"success_url": "https://merchant.example/cards?myquery=myparam",
"cancel_url": "https://merchant.example/cancel"
}
}
Example (Advanced)
Advanced example with all options, including card placement in the cascade order, vault limits, and page customization.
{
"type": "STORE_CARD",
"transaction_reference": "INV-2026-00042", // [optional] your reference for the fee charge; appears on statements and reporting
"signature": "secret-key-for-payload", // [optional] shared-secret signature for the payload, see the signature section below
"item": {
"title": "Save your card",
"description": "Securely store your card for future payments",
"amount": "50.00" // one-time fee charged with the 3DS verification; omit to default to "2.00"
},
// Placement of the newly stored card in the customer's cascade order
"card": {
"is_default": false, // [optional] default true for the customer's first card, false otherwise; when true, the existing default card is demoted
"priority": 2 // [optional] explicit fallback position among non-default cards; ignored when is_default is true; defaults to the end of the cascade order
},
"notification": {
"email": "me@my-email.co.za",
"webhook_url": "https://merchant.example/webhooks/cards"
},
"redirects": {
"success_url": "https://merchant.example/card-saved?myquery=myparam",
"cancel_url": "https://merchant.example/cancel"
},
"settings": {
"expiry_time": 1440 // minutes until the vault link lapses — omit when the link never expires (default)
},
"customization": {
"button_text": "Save card", // [optional] primary action button label
"type": "PAGE", // or "EMBED" for iframe-style hosting or in-app card storage
"is_display_cancel_button": true,
"confirmation_message": "Your card has been saved", // [optional] shown after successful storage; otherwise use redirects
"brand": {
"primary": "#00DC82",
"secondary": "#CCCCCC"
}
},
"metadata": {
"order_id": "ord_98765" // [optional] custom key/value pairs returned on the stored card and webhooks
}
}
Example (Embed card storage)
Embed card storage into your mobile app or frameworks like React, Angular or Vue.
{
"type": "STORE_CARD",
"item": {
"title": "Save your card"
},
"customization": {
"button_text": "Save card", // [optional]
"type": "EMBED", // show embedded screen that can be displayed in an 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 'card.cancelled' message
"brand": {
"primary": "#00DC82", // brand according to your app
"secondary": "#CCCCCC"
}
}
}
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 a Native App
The following message events will be sent to your app via a native bridge; use them to drive the next action:
card.loadedcard.cancelledcard.failedcard.storedcard.pendingcard.closed
The following data will be sent in the message on card.stored events to verify the stored card:
{
"source": "kwik-payments",
"event": "card.stored",
"sessionId": "ses_abc123...",
"customerId": "cus_abc123...",
"cardId": "crd_abc123...",
"transactionPublicId": "Abc123xyz...",
"status": "ACTIVE"
}
The following data will be sent in the message on card.failed events:
{
"source": "kwik-payments",
"event": "card.failed",
"sessionId": "ses_abc123...",
"customerId": "cus_abc123...",
"status": "DECLINED",
"message": "Your card could not be verified 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/card-vault/', async (req, res) => {
// TODO: Get the customerId stored from your session, token or DB that was created through the /customers API
const response = await fetch(`${KWIK_API}/card-vault/${customerId}/link`, {
method: 'POST',
headers,
body: JSON.stringify({
type: 'STORE_CARD',
item: {
title: 'My product name',
amount: '2.00' // initial charge amount, min R2.00
},
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 vault link' });
// The app only needs the URL to load
res.json({ vaultUrl: result.link_url });
});
// 2. Verify a transaction after the app receives card.stored
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 KwikVault({ onComplete, onCancel }) {
const [vaultUrl, setVaultUrl] = useState(null);
// Ask your backend to create the card-vault session
useEffect(() => {
(async () => {
const res = await fetch(`${BACKEND}/api/card-vault`, { method: 'POST' });
const { vaultUrl } = await res.json();
setVaultUrl(vaultUrl);
})();
}, []);
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: 'card.stored',
// sessionId: 'ses_...', transactionPublicId: 'tra_...',
// customerId: 'cus_...', status: 'PAID' }
switch (evt.event) {
case 'card.loaded':
break; // checkout ready
case 'card.pending':
break; // e.g. 3DS in progress — keep the WebView open
case 'card.stored': {
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 'card.failed':
break; // checkout shows its own retry UI — keep it open
case 'card.cancelled':
case 'card.closed':
onCancel();
break;
}
};
if (!vaultUrl) return <ActivityIndicator />;
return (
<WebView
source={{ uri: vaultUrl }}
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 KwikVault extends StatefulWidget {
final void Function(String transactionPublicId) onComplete;
final VoidCallback onCancel;
const KwikVault({
super.key,
required this.onComplete,
required this.onCancel,
});
@override
State<KwikVault> createState() => _KwikVaultState();
}
class _KwikVaultState extends State<KwikVault> {
String? vaultUrl;
@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/card-vault'));
final body = jsonDecode(res.body) as Map<String, dynamic>;
setState(() => vaultUrl = body['vaultUrl'] 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: 'card.stored',
// sessionId: 'ses_...', transactionPublicId: 'tra_...',
// customerId: 'cus_...', status: 'PAID' }
switch (evt['event']) {
case 'card.loaded':
break; // checkout ready
case 'card.pending':
break; // e.g. 3DS in progress — keep the WebView open
case 'card.stored':
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 'card.failed':
break; // checkout shows its own retry UI — keep it open
case 'card.cancelled':
case 'card.closed':
widget.onCancel();
break;
}
}
@override
Widget build(BuildContext context) {
if (vaultUrl == null) {
return const Center(child: CircularProgressIndicator());
}
return InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(vaultUrl!)),
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 KwikVaultViewController: 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/card-vault"))
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 vaultUrl = json["vaultUrl"] as? String,
let url = URL(string: vaultUrl) 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: 'card.stored',
// sessionId: 'ses_...', transactionPublicId: 'tra_...',
// customerId: 'cus_...', status: 'PAID' }
switch evt["event"] {
case "card.loaded":
break // checkout ready
case "card.pending":
break // e.g. 3DS in progress — keep the WebView open
case "card.stored":
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 "card.failed":
break // checkout shows its own retry UI — keep it open
case "card.cancelled", "card.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 KwikVaultActivity : 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 vaultUrl = withContext(Dispatchers.IO) {
val req = Request.Builder()
.url("$backend/api/card-vault")
.post(okhttp3.RequestBody.create(null, ByteArray(0)))
.build()
http.newCall(req).execute().use { res ->
JSONObject(res.body!!.string()).getString("vaultUrl")
}
}
webView.loadUrl(vaultUrl)
}
}
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=card.stored
// &sessionId=ses_...&transactionPublicId=tra_...
// &customerId=cus_...&status=PAID
when (uri.getQueryParameter("event")) {
"card.loaded" -> Unit // checkout ready
"card.pending" -> Unit // e.g. 3DS in progress — keep the WebView open
"card.stored" -> {
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.
}
}
"card.failed" -> Unit // checkout shows its own retry UI — keep it open
"card.cancelled", "card.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 { vaultUrl } = await fetch(`${BACKEND}/api/card-vault`, {
method: 'POST',
}).then((r) => r.json());
const iframe = document.createElement('iframe');
iframe.src = vaultUrl;
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 'card.stored': {
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 'card.cancelled':
case 'card.closed':
document.getElementById('checkout-container').replaceChildren();
break;
// card.loaded / card.pending / card.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.
Field | Required | Type | Description | Example |
|---|---|---|---|---|
| type | Y | ENUM | Vault mode: STORE_CARD (store a new card) or MANAGE_CARDS (view, add, remove, set default, reorder stored cards) | STORE_CARD |
| item | Y | Object | Payer-facing details for the vault page | See basic example |
| item.title | Y | String | Title shown on the vault page and payer statement | Save your card |
| item.description | N | String | Optional longer description shown on the vault page | Securely store your card for future payments |
| item.amount | N | String | One-time fee charged with the 3DS verification when the card is stored. Defaults to 2.00 (an amount is required for 3DS authentication) | 50.00 |
| transaction_reference | N | String(35) | Your reference for the verification / fee charge; appears on statements and reporting | INV-2026-00042 |
| card | N | Object | Placement of the newly stored card in the customer's cascade order (STORE_CARD only) | See advanced example |
| card.is_default | N | Boolean | Make the new card the customer's default. Defaults to true for the customer's first card, false otherwise. When true, the existing default is demoted | false |
| card.priority | N | Integer | Explicit fallback position among non-default cards; ignored when is_default is true. Defaults to the end of the cascade order | 2 |
| items | N | Array | Invoice line items from your catalog when generating an invoice for the fee charge; overrides the default single line from item — see products | See one-time fee example |
| items.product_id | Y | String | Catalog product for this line | pro_abc123... |
| items.qty | N | Integer | Quantity for this line item (default 1) | 1 |
| items.price_excl | N | Number | Override the default product amount (excluding tax) | 300.23 |
| invoice.is_generate | N | Boolean | Whether to generate an invoice for the fee charge | true |
| invoice.is_send | N | Boolean | Whether to send the paid invoice to the customer | true |
| manage | C | Object | Payer permissions when type is MANAGE_CARDS | See manage example |
| manage.is_allow_add | N | Boolean | Payer can add a new card; runs the R2.00 / fee 3DS flow. Default true | true |
| manage.is_allow_remove | N | Boolean | Payer can remove stored cards; removing the default promotes the next card by priority. Default true | true |
| manage.is_allow_set_default | N | Boolean | Payer can choose their default card and reorder fallbacks. Default true | true |
| manage.max_cards | N | Integer | Cap on stored cards for this customer; the link shows an error state when the customer is at the limit. Omit for no limit | 3 |
| manage.is_duplicate_check | N | Boolean | Rejects storing a card whose PAN fingerprint already exists on this customer, preventing the same card occupying two cascade slots. Default true | true |
| signature | N | String | Optional passphrase signature for verifying the payload | secret-key-for-payload |
| notification.email | N | String | Email address to notify when a card is stored | me@my-email.co.za |
| notification.webhook_url | N | String | Webhook URL for card vault events | https://merchant.example/webhooks/cards |
| redirects.success_url | N | String | Redirect after the card is stored or changes are saved; &signature= appended if configured | https://merchant.example/card-saved?myquery=myparam |
| redirects.cancel_url | N | String | Redirect if the payer cancels | https://merchant.example/cancel |
| settings.expiry_time | N | Integer | Link lifetime in minutes; omit so the link does not expire (default) | 1440 |
| customization.button_text | N | String | Primary action button label | Save card |
| customization.confirmation_message | N | String | Message after successful storage; otherwise use redirects | Your card has been saved |
| customization.type | N | ENUM | Create a link for a PAGE or pass through EMBED for iframe or in-app card storage. The EMBED type pages will not redirect you but transmit a message via the window postMessage() method | PAGE |
| customization.embed_channel | N | ENUM | Where an EMBED card storage page 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.brand.primary | N | String | Primary hex colour | #00DC82 |
| customization.brand.secondary | N | String | Secondary hex colour | #CCCCCC |
| metadata | N | Object | Custom key/value metadata returned on the stored card and webhooks | {"order_id": "ord_98765"} |
Response Body
{
"status": true,
"result": {
"id": "vlt_HVpCeoNys1f22X7QcuWHY",
"session_id": "ses_G-xkVKoxHgEBrY8suKgR3",
"customer_id": "cus_abc123...",
"type": "STORE_CARD",
"amount": "50.00",
"currency": "ZAR",
"transaction_reference": "INV-2026-00042",
"link_url": "https://pay.kwik.co.za/card-vault/cs_test_a1b2c3",
"expires_at": "2026-07-06T12:30:00Z"
}
}
Response Parameters
Field | Type | Description | Example |
|---|---|---|---|
| status | Boolean | Whether the request succeeded | true |
| result.id | String(32) | Unique card vault link identifier | vlt_HVpCeoNys1f22X7QcuWHY |
| result.session_id | String(32) | Session identifier for the vault link | ses_G-xkVKoxHgEBrY8suKgR3 |
| result.customer_id | String(32) | Associated customer ID | cus_abc123... |
| result.type | ENUM | Vault mode for this link | STORE_CARD |
| result.amount | String | Amount charged when the card is stored | 2.00 |
| result.currency | String(3) | ISO 4217 currency code | ZAR |
| result.transaction_reference | String(35) | Your reference for the verification / fee charge; null when not supplied | INV-2026-00042 |
| result.link_url | String | URL to redirect the payer to store or manage cards | https://pay.kwik.co.za/card-vault/cs_test_a1b2c3 |
| result.expires_at | String | ISO timestamp when the vault link expires; null when the link does not expire | 2026-07-06T12:30:00Z |
Signature creation
When creating API keys on the dashboard you can download a passphrase key, use it to generate your signature and send it in the signature parameter. The canonicalization and HMAC-SHA256 process is identical across all endpoints — see Checkout Link — Signature creation for Node.js, PHP, C#, Java, and Python examples.
Webhook
When a card is stored, fails verification, is removed, or the customer's card order changes, 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 card.stored, card.failed, card.removed, and others listed under Webhook events.
Webhook Payload
{
// card.stored · card.failed · card.removed · card.default_changed · card.reordered · card.link_expired
"event": "card.stored",
"data": [
{
"card": {
"id": "crd_SFq2E9LskQimPkf2mRqnV",
"customer_id": "cus_abc123...",
"session_id": "ses_G-xkVKoxHgEBrY8suKgR3",
"brand": "VISA",
"last_four": "4242",
"expiry_month": "09",
"expiry_year": "2029",
"holder": "J DOE",
"card_status": "ACTIVE",
"is_default": false,
"priority": 2,
"amount": "2.00",
"transaction_reference": "INV-2026-00042",
"transaction_id": "tra_pr6CvR_4pvWwmgQ4y3dtY", // 3DS verification / transaction (the card's CIT anchor for card-on-file charges)
"metadata": {
"order_id": "ord_98765"
},
"created_at": "2026-07-06T10:15:30Z"
}
}
],
"created_at": "2026-07-06T10:15:30Z"
}
Webhook Payload Parameters
Field | Type | Description | Example |
|---|---|---|---|
| event | String | Type of webhook event that occurred | card.stored |
| data | Array | Array containing card data | ... |
| data.card.id | String(32) | Unique card identifier | crd_SFq2E9LskQimPkf2mRqnV |
| data.card.customer_id | String(32) | Associated customer ID | cus_abc123... |
| data.card.session_id | String(32) | Vault link session that produced this event | ses_G-xkVKoxHgEBrY8suKgR3 |
| data.card.brand | ENUM | Card brand, see lookups | VISA |
| data.card.last_four | String(4) | Last four digits of the card number | 4242 |
| data.card.expiry_month | String(2) | Card expiry month | 09 |
| data.card.expiry_year | String(4) | Card expiry year | 2029 |
| data.card.holder | String | Cardholder name | J DOE |
| data.card.card_status | ENUM | Card status, see lookups | ACTIVE |
| data.card.is_default | Boolean | Whether this card is the customer's default | false |
| data.card.priority | Integer | Resolved position in the customer's cascade order | 2 |
| data.card.amount | String | Amount charged when the card was stored | 2.00 |
| data.card.transaction_reference | String(35) | Your reference for the verification / fee charge; null when not supplied | INV-2026-00042 |
| data.card.transaction_id | String | Transaction identifier of the 3DS verification / amount charge | tra_pr6CvR_4pvWwmgQ4y3dtY |
| data.card.metadata | Object | Custom key/value metadata supplied on create | {"order_id": "ord_98765"} |
| data.card.created_at | String | ISO timestamp when the card was stored | 2026-07-06T10:15:30Z |
| created_at | String | ISO timestamp when webhook was created | 2026-07-06T10:15:30Z |
Webhook Events
| Event | Description | Trigger Condition | Data Included |
|---|---|---|---|
card.stored | A card was successfully verified and stored in the vault | When 3DS authentication and the verification / fee charge succeed | Card details, fee amount, fee transaction |
card.failed | Card storage failed | When 3DS authentication fails or the verification / fee charge is declined | Card attempt details, error information |
card.removed | A card was removed from the vault | When the payer removes a card on a MANAGE_CARDS link, or via the API | Removed card details; promoted_card_id when the default changed as a result |
card.default_changed | The customer's default card changed | When the payer or merchant sets a new default card | previous_card_id and new default card details |
card.reordered | The customer's cascade order changed | When the payer or merchant reorders fallback cards | Full resolved cascade order |
card.link_expired | The vault link expired without completion | When the link reaches settings.expiry_time without a card being stored | Link details only, no card data |
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.
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.
Card Vault Charge
Charge a customer's stored cards server-to-server. Attempts the default card first with automatic cascade to fallback cards, or targets a specific card. Supports merchant-initiated (MIT) scheme compliance, invoice issuance, and webhook notifications.