Checkout Form
Request
Request to generate a checkout page session
Path
POST /checkout/form
Example (Basic)
Basic example to create a one-time payment form with a fixed amount.
<form action="[path]/checkout/form" method="post">
<input type="hidden" name="public_key" value="<INSERT YOUR PUBLIC KEY>">
<input type="hidden" name="type" value="CHOOSE_WHAT_TO_PAY">
<!-- item: title on checkout / invoice -->
<input type="hidden" name="item_title" value="My product name">
<!-- [optional] default ONE_TIME when item_frequency omitted -->
<input type="hidden" name="item_frequency" value="ONE_TIME">
<!-- [optional] preset an amount to pay else an amount field will show for the user to enter -->
<input type="hidden" name="item_amount" value="1200.00">
<input type="submit" value="Pay">
</form>
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.
<form action="[path]/checkout/form" method="post">
<input type="hidden" name="public_key" value="<INSERT YOUR PUBLIC KEY>">
<input type="hidden" name="type" value="CHOOSE_WHAT_TO_PAY">
<!-- Pass your product / campaign details via item -->
<input type="hidden" name="item_title" value="My product name">
<input type="hidden" name="item_description" value="My product description">
<!-- [optional] options the payer may choose among -->
<input type="hidden" name="item_frequency" value="ONE_TIME,MONTHLY,ANNUALLY">
<!-- [optional] preset amounts (max five) -->
<input type="hidden" name="item_amount_options" value="50,100,150,200,250">
<!-- [optional] cap when payer enters any amount -->
<input type="hidden" name="item_amount_max" value="3000">
<input type="hidden" name="customization_button_text" value="Donate now">
<input type="hidden" name="customization_confirmation_message" value="Thank you for your donation">
<!-- [optional] when API keys/signing are configured -->
<input type="hidden" name="signature" value="secret-key-for-payload">
<input type="hidden" name="notification_email" value="me@my-email.co.za">
<input type="hidden" name="notification_webhook_url" value="https://merchant.example/webhooks/payments">
<input type="hidden" name="invoice_is_generate" value="true">
<input type="hidden" name="invoice_is_send" value="true">
<!-- Comma-separated enums when no customer_id / prefilled customer (name, surname, email always captured) -->
<input type="hidden" name="customer_fields" value="CONTACT_NUMBER,ID_NUMBER,COMPANY_NAME,COMPANY_REG,VAT_NUMBER,BILLING_ADDRESS,SHIPPING_ADDRESS">
<input type="submit" value="Donate">
</form>
Example (Products, subscriptions and invoices)
Example with a recurring and one-time product to create a subscription with invoices.
<form action="[path]/checkout/form" method="post">
<input type="hidden" name="public_key" value="<INSERT YOUR PUBLIC KEY>">
<input type="hidden" name="type" value="PRODUCTS_AND_SUBSCRIPTIONS">
<input type="hidden" name="customization_button_text" value="Pay now">
<!-- [optional] payload signature when configured -->
<input type="hidden" name="signature" value="secret-key-for-payload">
<input type="hidden" name="customer_id" value="cus_abc123...">
<input type="hidden" name="items_0_product_id" value="pro_abc123...">
<input type="hidden" name="items_0_qty" value="2">
<input type="hidden" name="items_1_product_id" value="pro_xyz789...">
<!-- items_1_qty omitted → default qty 1 -->
<input type="hidden" name="invoice_is_generate" value="true">
<input type="hidden" name="invoice_is_send" value="true">
<input type="hidden" name="redirects_success_url" value="https://merchant.example/success?myquery=myparam">
<input type="hidden" name="redirects_cancel_url" value="https://merchant.example/cancel">
<input type="submit" value="Pay">
</form>
Example (Advanced )
Advanced example with example with all options
<form action="[path]/checkout/form" method="post">
<input type="hidden" name="public_key" value="<INSERT YOUR PUBLIC KEY>">
<input type="hidden" name="type" value="PRODUCTS_AND_SUBSCRIPTIONS">
<!-- [optional] shared-secret signature for the payload, see the signature section below -->
<input type="hidden" name="signature" value="secret-key-for-payload">
<input type="hidden" name="customer_reference" value="CLN240919000001">
<input type="hidden" name="customer_company_name" value="">
<input type="hidden" name="customer_person_name" value="John">
<input type="hidden" name="customer_person_surname" value="Doe">
<input type="hidden" name="customer_client_type" value="RESIDENT_INDIVIDUAL">
<input type="hidden" name="customer_id_type" value="SOUTH_AFRICAN_ID">
<input type="hidden" name="customer_id_number" value="8007014800087">
<input type="hidden" name="customer_email" value="johndoe@mail.com">
<input type="hidden" name="customer_contact_number" value="+27831234567">
<input type="hidden" name="customer_billing_address_line1" value="1 Loop Street">
<input type="hidden" name="customer_billing_address_line2" value="Floor 5">
<input type="hidden" name="customer_billing_address_city_or_town" value="Cape Town">
<input type="hidden" name="customer_billing_address_province_or_state" value="Western Cape">
<input type="hidden" name="customer_billing_address_postal_or_zip_code" value="8001">
<input type="hidden" name="customer_billing_address_country" value="ZA">
<input type="hidden" name="customer_shipping_contact_name" value="Jane Doe">
<input type="hidden" name="customer_shipping_contact_number" value="+27820000000">
<input type="hidden" name="customer_shipping_address_line1" value="1 Loop Street">
<input type="hidden" name="customer_shipping_address_line2" value="Floor 5">
<input type="hidden" name="customer_shipping_address_city_or_town" value="Cape Town">
<input type="hidden" name="customer_shipping_address_province_or_state" value="Western Cape">
<input type="hidden" name="customer_shipping_address_postal_or_zip_code" value="8001">
<input type="hidden" name="customer_shipping_address_country" value="ZA">
<input type="hidden" name="customer_metadata_custom_field_1" value="custom data 1">
<input type="hidden" name="customer_metadata_custom_field_2" value="custom data 2">
<!-- catalogue lines -->
<input type="hidden" name="items_0_product_id" value="pro_abc123...">
<input type="hidden" name="items_0_qty" value="2">
<input type="hidden" name="items_0_discount" value="100">
<input type="hidden" name="items_1_product_id" value="pro_xyz789...">
<input type="hidden" name="items_1_qty" value="3">
<!-- inline product row (omit items_2_product_id) -->
<input type="hidden" name="items_2_name" value="Generic Wooden Ball">
<input type="hidden" name="items_2_description" value="Good For Training And Recreational Purposes">
<input type="hidden" name="items_2_code" value="PRD-606075">
<input type="hidden" name="items_2_unit_label" value="each">
<input type="hidden" name="items_2_price_excl" value="2409.86">
<input type="hidden" name="items_2_is_tax" value="false">
<input type="hidden" name="items_2_pricing_type" value="RECURRING">
<input type="hidden" name="items_2_product_status" value="ACTIVE">
<input type="hidden" name="items_2_billing_period" value="MONTHLY">
<input type="hidden" name="items_2_custom_period" value="">
<input type="hidden" name="items_2_custom_interval" value="">
<input type="hidden" name="items_2_picture_default" value="">
<input type="hidden" name="invoice_is_generate" value="true">
<input type="hidden" name="invoice_is_send" value="true">
<!-- comma-separated enums: CARD,ZAPPER,... -->
<input type="hidden" name="payment_methods" value="CARD,ZAPPER">
<!-- Successful redirect URLs may append `signature` when signatures are configured. -->
<input type="hidden" name="redirects_success_url" value="https://merchant.example/success?myquery=myparam">
<input type="hidden" name="redirects_cancel_url" value="https://merchant.example/cancel">
<input type="hidden" name="settings_expiry_time" value="1440">
<input type="hidden" name="customization_button_text" value="Pay">
<!-- page-style checkout only; embed / iframe payloads are documented on Checkout Link, not duplicated here -->
<input type="hidden" name="customization_is_display_cancel_button" value="true">
<input type="hidden" name="customization_is_display_total" value="true">
<input type="hidden" name="customization_is_display_items" value="true">
<input type="hidden" name="customization_brand_primary" value="#00DC82">
<input type="hidden" name="customization_brand_secondary" value="#CCCCCC">
<input type="submit" value="Pay">
</form>
Request Parameters
Form input names mirror the Checkout Link request parameters. Y applies to forms that POST a checkout; C depends on type and whether you send customer_id vs prefixed customer_* fields.
Field (input name) | Required | Type | Description | Example |
|---|---|---|---|---|
| public_key | Y | String | Public API key (dashboard / integrations). Always include. | <INSERT YOUR PUBLIC KEY> |
| type | Y | ENUM | Same as Checkout Link: CHOOSE_WHAT_TO_PAY or PRODUCTS_AND_SUBSCRIPTIONS. | CHOOSE_WHAT_TO_PAY |
| item_title | C | String | Flexible checkout title (invoice / UI). | My product name |
| item_description | N | String | Flexible checkout description. | My product description |
| item_frequency | N | String | Comma-separated frequency enums (ONE_TIME, MONTHLY, …). Default is ONE_TIME. | ONE_TIME,MONTHLY,ANNUALLY |
| item_amount | N | String | Preset amount; omit so payer enters amount. | 1200.00 |
| item_amount_options | N | String | Comma-separated preset amounts (max five values, matches Checkout Link). | 50,100,150,200,250 |
| item_amount_max | N | Number | Cap when payer enters any amount. | 3000 |
| signature | N | String | Payload signature when your integration signs requests. | secret-key-for-payload |
| items__product_id | C | String | nth line item catalogue id (n zero-based). Omit for an inline product row on that index. See products. | pro_abc123... |
| items__qty | N | Integer | Quantity for nth line (default 1 when omitted). | 2 |
| items__discount | N | Number | Discount for nth catalogue line (product_id present). | 100 |
| items__name | C | String | Inline line only: product name when items_{n}_product_id is absent. | Generic Wooden Ball |
| items__description | N | String | Inline line description. | Good For Training … |
| items__code | C | String | Inline line unique code. | PRD-606075 |
| items__unit_label | N | String | Inline unit label. | each |
| items__price_excl | C | String | Inline price excluding tax. | 2409.86 |
| items__is_tax | N | Boolean | Inline tax flag. | false |
| items__pricing_type | C | ENUM | Inline pricing type (RECURRING, etc.). | RECURRING |
| items__product_status | N | ENUM | e.g. ACTIVE. | ACTIVE |
| items__billing_period | C | String | When recurring. | MONTHLY |
| items__custom_period | N | String | Optional opaque period value. | |
| items__custom_interval | N | String | Optional opaque interval value. | |
| items__picture_default | N | String | Optional default picture reference. | |
| customer_id | C | String | Existing customer instead of prefixed customer_* body. | cus_abc123... |
| customer_fields | N | String | Comma-separated field tokens when payer form should collect extras (LINK enum list — see Checkout Link). Uppercase enums as in JSON. | CONTACT_NUMBER,ID_NUMBER,… |
| customer_reference … | C | Prefix | Same nested customer fields as Checkout Link flattened: customer_person_name, customer_billing_address_line1, customer_metadata_{key} (underscore key segments), etc. | See advanced HTML example |
| invoice_is_generate | C | Boolean | Generate invoice (true / false). | true |
| invoice_is_send | N | Boolean | Send paid invoice (true / false). | true |
| notification_email | N | String | Notify email on successful payment. | me@my-email.co.za |
| notification_webhook_url | N | String | Webhook URL for successful payment notifications (same semantics as Checkout Link notification.webhook_url). | https://merchant.example/webhooks/payments |
| redirects_success_url | N | String | Success redirect (redirects.success_url in JSON). | https://merchant.example/success |
| redirects_cancel_url | N | String | Cancel redirect (redirects.cancel_url in JSON). | https://merchant.example/cancel |
| payment_methods | N | String | Comma-separated method codes (CARD, ZAPPER, …) — mirrors payment.payment_methods JSON array. | CARD,ZAPPER |
| settings_expiry_time | N | Integer | Minutes until checkout link/session expiry (settings.expiry_time). | 1440 |
| customization_button_text | N | String | Primary button label. | Pay |
| customization_confirmation_message | N | String | Message after payment (flexible flows). | Thank you… |
| 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 |
For full JSON-shape definitions (enums, array lengths, behavioural notes), use Checkout Link alongside this flattened mapping.
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 express = require('express')
const crypto = require('crypto')
const app = express()
function generateSignature(params, passphrase) {
const canonical = Object.keys(params)
.sort()
.map(k => `${k}=${params[k]}`)
.join('&')
return crypto
.createHmac('sha256', passphrase)
.update(canonical)
.digest('hex')
}
app.get('/checkout', (req, res) => {
const params = {
public_key: 'pk_test_123',
type: 'CHOOSE_WHAT_TO_PAY',
item_title: 'My product name',
item_frequency: 'ONE_TIME',
item_amount: '1200.00'
}
const signature = generateSignature(params, 'your-passphrase')
res.send(`
<form action="https://api.kwik.co.za/checkout/form" method="post">
${Object.entries(params).map(([k,v]) => `<input type="hidden" name="${k}" value="${v}">`).join('')}
<input type="hidden" name="signature" value="${signature}">
<button type="submit">Pay</button>
</form>
`)
})
app.listen(3000)
<?php
function generateSignature($params, $passphrase) {
ksort($params);
$canonical = [];
foreach ($params as $k => $v) {
$canonical[] = "$k=$v";
}
return hash_hmac('sha256', implode('&', $canonical), $passphrase);
}
$params = [
"public_key" => "pk_test_123",
"type" => "CHOOSE_WHAT_TO_PAY",
"item_title" => "My product name",
"item_frequency" => "ONE_TIME",
"item_amount" => "1200.00"
];
$signature = generateSignature($params, "your-passphrase");
?>
<form action="https://api.kwik.co.za/checkout/form" method="post">
<?php foreach ($params as $key => $value): ?>
<input type="hidden" name="<?= $key ?>" value="<?= $value ?>">
<?php endforeach; ?>
<input type="hidden" name="signature" value="<?= $signature ?>">
<input type="submit" value="Pay">
</form>
using System;
@using System.Security.Cryptography
@using System.Text
@functions {
string GenerateSignature(Dictionary<string, string> parameters, string passphrase)
{
var canonical = string.Join("&", parameters
.OrderBy(x => x.Key, StringComparer.Ordinal)
.Select(x => $"{x.Key}={x.Value}"));
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(passphrase));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
return Convert.ToHexString(hash).ToLowerInvariant();
}
}
@{
var parameters = new Dictionary<string, string>
{
["public_key"] = "pk_test_123",
["type"] = "CHOOSE_WHAT_TO_PAY",
["item_title"] = "My product name",
["item_frequency"] = "ONE_TIME",
["item_amount"] = "1200.00"
};
var passphrase = "your-passphrase";
var signature = GenerateSignature(parameters, passphrase);
}
<form action="https://api.kwik.co.za/checkout/form" method="post">
@foreach (var field in parameters)
{
<input type="hidden" name="@field.Key" value="@field.Value" />
}
<input type="hidden" name="signature" value="@signature" />
<input type="submit" value="Pay" />
</form>
<%@ page import="javax.crypto.Mac" %>
<%@ page import="javax.crypto.spec.SecretKeySpec" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page import="java.util.*" %>
<%!
public String generateSignature(Map<String, String> parameters, String passphrase) throws Exception {
List<String> keys = new ArrayList<>(parameters.keySet());
Collections.sort(keys);
List<String> parts = new ArrayList<>();
for (String key : keys) {
parts.add(key + "=" + parameters.get(key));
}
String canonical = String.join("&", parts);
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();
}
%>
<%
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("public_key", "pk_test_123");
parameters.put("type", "CHOOSE_WHAT_TO_PAY");
parameters.put("item_title", "My product name");
parameters.put("item_frequency", "ONE_TIME");
parameters.put("item_amount", "1200.00");
String passphrase = "your-passphrase";
String signature = generateSignature(parameters, passphrase);
%>
<form action="https://api.kwik.co.za/checkout/form" method="post">
<% for (Map.Entry<String, String> field : parameters.entrySet()) { %>
<input type="hidden" name="<%= field.getKey() %>" value="<%= field.getValue() %>">
<% } %>
<input type="hidden" name="signature" value="<%= signature %>">
<input type="submit" value="Pay">
</form>
from flask import Flask, render_template_string
import hmac, hashlib
app = Flask(__name__)
def generate_signature(params, passphrase):
canonical = "&".join(f"{k}={params[k]}" for k in sorted(params))
return hmac.new(passphrase.encode(), canonical.encode(), hashlib.sha256).hexdigest()
@app.route("/checkout")
def checkout():
params = {
"public_key": "pk_test_123",
"type": "CHOOSE_WHAT_TO_PAY",
"item_title": "My product name",
"item_frequency": "ONE_TIME",
"item_amount": "1200.00"
}
signature = generate_signature(params, "your-passphrase")
inputs = "".join(
f'<input type="hidden" name="{k}" value="{v}">'
for k, v in params.items()
)
return render_template_string(f"""
<form action="https://api.kwik.co.za/checkout/form" method="post">
{inputs}
<input type="hidden" name="signature" value="{signature}">
<button type="submit">Pay</button>
</form>
""")
app.run()
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",
"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.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.
Checkout Link
Create a secure, customizable checkout link 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.
Electronic Mandates
Hosted web pages that new customers can use to signup with or existing customers can use to add collections to their account.