Money In

Card Vault

 

Create a secure, customizable link session for a customer to store or manage cards in the card vault. Cards are tokenized and verified with 3D Secure authentication, with an optional one-time fee, support for default and fallback card ordering, and webhook notifications.

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 ParameterTypeDescriptionExample
customer_idString(32)The customer to store or manage cards for. The customer must exist — see customerscus_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"
    }
  }
}

The following message events will be sent to your app via a native bridge; use them to drive the next action:

  • card.loaded
  • card.cancelled
  • card.failed
  • card.stored
  • card.pending
  • card.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"
}
See Checkout Link for how the integration works for embedded payments.

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
RequiredType
Description
Example
typeYENUMVault mode: STORE_CARD (store a new card) or MANAGE_CARDS (view, add, remove, set default, reorder stored cards)STORE_CARD
itemYObjectPayer-facing details for the vault pageSee basic example
item.titleYStringTitle shown on the vault page and payer statementSave your card
item.descriptionNStringOptional longer description shown on the vault pageSecurely store your card for future payments
item.amountNStringOne-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_referenceNString(35)Your reference for the verification / fee charge; appears on statements and reportingINV-2026-00042
cardNObjectPlacement of the newly stored card in the customer's cascade order (STORE_CARD only)See advanced example
card.is_defaultNBooleanMake the new card the customer's default. Defaults to true for the customer's first card, false otherwise. When true, the existing default is demotedfalse
card.priorityNIntegerExplicit fallback position among non-default cards; ignored when is_default is true. Defaults to the end of the cascade order2
itemsNArrayInvoice line items from your catalog when generating an invoice for the fee charge; overrides the default single line from item — see productsSee one-time fee example
items.product_idYStringCatalog product for this linepro_abc123...
items.qtyNIntegerQuantity for this line item (default 1)1
items.price_exclNNumberOverride the default product amount (excluding tax)300.23
invoice.is_generateNBooleanWhether to generate an invoice for the fee chargetrue
invoice.is_sendNBooleanWhether to send the paid invoice to the customertrue
manageCObjectPayer permissions when type is MANAGE_CARDSSee manage example
manage.is_allow_addNBooleanPayer can add a new card; runs the R2.00 / fee 3DS flow. Default truetrue
manage.is_allow_removeNBooleanPayer can remove stored cards; removing the default promotes the next card by priority. Default truetrue
manage.is_allow_set_defaultNBooleanPayer can choose their default card and reorder fallbacks. Default truetrue
manage.max_cardsNIntegerCap on stored cards for this customer; the link shows an error state when the customer is at the limit. Omit for no limit3
manage.is_duplicate_checkNBooleanRejects storing a card whose PAN fingerprint already exists on this customer, preventing the same card occupying two cascade slots. Default truetrue
signatureNStringOptional passphrase signature for verifying the payloadsecret-key-for-payload
notification.emailNStringEmail address to notify when a card is storedme@my-email.co.za
notification.webhook_urlNStringWebhook URL for card vault eventshttps://merchant.example/webhooks/cards
redirects.success_urlNStringRedirect after the card is stored or changes are saved; &signature= appended if configuredhttps://merchant.example/card-saved?myquery=myparam
redirects.cancel_urlNStringRedirect if the payer cancelshttps://merchant.example/cancel
settings.expiry_timeNIntegerLink lifetime in minutes; omit so the link does not expire (default)1440
customization.button_textNStringPrimary action button labelSave card
customization.confirmation_messageNStringMessage after successful storage; otherwise use redirectsYour card has been saved
customization.typeNENUMCreate 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() methodPAGE
customization.embed_channelNENUMWhere 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 EMBEDAPP
customization.is_display_cancel_buttonNBooleanShow cancel controltrue
customization.brand.primaryNStringPrimary hex colour#00DC82
customization.brand.secondaryNStringSecondary hex colour#CCCCCC
metadataNObjectCustom 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
statusBooleanWhether the request succeededtrue
result.idString(32)Unique card vault link identifiervlt_HVpCeoNys1f22X7QcuWHY
result.session_idString(32)Session identifier for the vault linkses_G-xkVKoxHgEBrY8suKgR3
result.customer_idString(32)Associated customer IDcus_abc123...
result.typeENUMVault mode for this linkSTORE_CARD
result.amountStringAmount charged when the card is stored2.00
result.currencyString(3)ISO 4217 currency codeZAR
result.transaction_referenceString(35)Your reference for the verification / fee charge; null when not suppliedINV-2026-00042
result.link_urlStringURL to redirect the payer to store or manage cardshttps://pay.kwik.co.za/card-vault/cs_test_a1b2c3
result.expires_atStringISO timestamp when the vault link expires; null when the link does not expire2026-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
eventStringType of webhook event that occurredcard.stored
dataArrayArray containing card data...
data.card.idString(32)Unique card identifiercrd_SFq2E9LskQimPkf2mRqnV
data.card.customer_idString(32)Associated customer IDcus_abc123...
data.card.session_idString(32)Vault link session that produced this eventses_G-xkVKoxHgEBrY8suKgR3
data.card.brandENUMCard brand, see lookupsVISA
data.card.last_fourString(4)Last four digits of the card number4242
data.card.expiry_monthString(2)Card expiry month09
data.card.expiry_yearString(4)Card expiry year2029
data.card.holderStringCardholder nameJ DOE
data.card.card_statusENUMCard status, see lookupsACTIVE
data.card.is_defaultBooleanWhether this card is the customer's defaultfalse
data.card.priorityIntegerResolved position in the customer's cascade order2
data.card.amountStringAmount charged when the card was stored2.00
data.card.transaction_referenceString(35)Your reference for the verification / fee charge; null when not suppliedINV-2026-00042
data.card.transaction_idStringTransaction identifier of the 3DS verification / amount chargetra_pr6CvR_4pvWwmgQ4y3dtY
data.card.metadataObjectCustom key/value metadata supplied on create{"order_id": "ord_98765"}
data.card.created_atStringISO timestamp when the card was stored2026-07-06T10:15:30Z
created_atStringISO timestamp when webhook was created2026-07-06T10:15:30Z

Webhook Events

EventDescriptionTrigger ConditionData Included
card.storedA card was successfully verified and stored in the vaultWhen 3DS authentication and the verification / fee charge succeedCard details, fee amount, fee transaction
card.failedCard storage failedWhen 3DS authentication fails or the verification / fee charge is declinedCard attempt details, error information
card.removedA card was removed from the vaultWhen the payer removes a card on a MANAGE_CARDS link, or via the APIRemoved card details; promoted_card_id when the default changed as a result
card.default_changedThe customer's default card changedWhen the payer or merchant sets a new default cardprevious_card_id and new default card details
card.reorderedThe customer's cascade order changedWhen the payer or merchant reorders fallback cardsFull resolved cascade order
card.link_expiredThe vault link expired without completionWhen the link reaches settings.expiry_time without a card being storedLink details only, no card data

Webhook Security

All webhooks are sent with the following headers for verification:

  • X-Signature: HMAC-SHA256 signature of the payload
  • X-Timestamp: Unix timestamp of when the webhook was sent
  • User-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.