Card Vault Link

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 fee.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.
{
"type": "STORE_CARD",
"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]
},
"fee": {
"amount": "50.00", // one-time fee charged with the 3DS verification; omit to default to "2.00"
"is_refund": false // [optional] default false — when true, the default R2.00 verification amount is automatically reversed after successful storage; ignored when a custom fee amount is set
},
"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
},
"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",
"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"
},
// 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
},
"fee": {
"amount": "50.00",
"is_refund": false
},
"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)
"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
},
"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"
}
}
}
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
"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"
}
}
}
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...",
"transactionId": "tra_abc123...",
"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...",
"transactionId": "tra_abc123...",
"status": "DECLINED",
"message": "Your card could not be verified due to insufficient funds"
}
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 |
| 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 |
| fee.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 |
| fee.is_refund | N | Boolean | When true, the default R2.00 verification amount is automatically reversed after successful storage. Ignored when a custom fee.amount is set. Default false | false |
| 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 |
| 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 |
| settings.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 |
| settings.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 |
| 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.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 |
Response Body
{
"status": true,
"result": {
"id": "cdl_HVpCeoNys1f22X7QcuWHY",
"session_id": "ses_G-xkVKoxHgEBrY8suKgR3",
"customer_id": "cus_abc123...",
"type": "STORE_CARD",
"fee_amount": "2.00",
"currency": "ZAR",
"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 | cdl_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.fee_amount | String | Fee or verification amount charged when the card is stored | 2.00 |
| result.currency | String(3) | ISO 4217 currency code | ZAR |
| 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,
"fee_amount": "2.00",
"fee_transaction_id": "tra_pr6CvR_4pvWwmgQ4y3dtY", // 3DS verification / fee transaction (the card's CIT anchor for card-on-file charges)
"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.fee_amount | String | Fee or verification amount charged when the card was stored | 2.00 |
| data.card.fee_transaction_id | String | Transaction identifier of the 3DS verification / fee charge | tra_pr6CvR_4pvWwmgQ4y3dtY |
| 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, synchronous and asynchronous processing, and webhook notifications.