Payment Links
A payment link is a one-time checkout URL that ties a seller, a set of line items, and an amount together. Send it to the buyer — they verify identity and pay without any further involvement from your server.
When to use payment links
Buyer selects a product, your server generates the link, buyer pays via WhatsApp OTP.
Send a client a link for a service invoice. Link expires in 24 hours if unpaid.
One registered user pays another via a single link — no card details required.
Each billing cycle needs a new link — payment links are single-use only.
Generate a payment link
Call POST /v1/payments/generate-escrow-payment-link from your server. The response includes a paymentUrl — send this to the buyer.
Always include an x-idempotency-key header. Duplicate requests with the same key return the original response instead of creating a second link — safe to retry on network errors.
Amounts are in cents. $250.00 USD = 25000. ZiG 100.00 = 10000. Never pass decimals.
curl -X POST "https://api.switchlock.co.zw/v1/payments/generate-escrow-payment-link" \
-H "x-api-key: sl_live_a1b2c3d4e5f6..." \
-H "x-company-id: cmp_01HXYZ..." \
-H "x-idempotency-key: order-001-2026-06-23" \
-H "Content-Type: application/json" \
-d '{
"sellerId": "usr_01HSELLER...",
"buyerId": "usr_01HBUYER...",
"amount": 35000,
"currency": "USD",
"externalReference": "ORDER-001",
"userReference": "INV-2026-001",
"items": [
{
"name": "Laptop Stand",
"description": "Aluminium adjustable stand",
"quantity": 1,
"pricePerUnit": 25000
},
{
"name": "USB-C Hub",
"description": "7-in-1 hub",
"quantity": 2,
"pricePerUnit": 5000
}
],
"splitPlan": {
"system_fee_cents": 700,
"service_fee_cents": 300,
"seller_payout_cents": 34000
}
}'| Field | Type | Required | Notes |
|---|---|---|---|
| buyerId | UUID | Yes | The buyer's user ID within your company |
| amount | integer (cents) | Yes | Total amount. Min 1. Must equal sum of items × quantity. |
| currency | USD | ZIG | Yes | Currency for this transaction |
| items | InvoiceItemDto[] | Yes | At least one item required. See sub-table below. |
| sellerId | UUID | Developer model only | Must belong to the same companyId. Omit for Merchant model. |
| externalReference | string | No | Your internal order/invoice ID — stored on the transaction |
| userReference | string | No | Used for ledger lookup fallback if buyerId lookup fails |
| splitPlan | object | No | Override default fee split. See sub-table below. |
| Field | Required |
|---|---|
| name | Yes |
| description | No |
| quantity | Yes — min 1 |
| pricePerUnit | Yes — min 1 (cents) |
| Field | Notes |
|---|---|
| system_fee_cents | Platform fee (IMTT / GL) |
| service_fee_cents | Your service commission |
| seller_payout_cents | What the seller receives |
{
"id": "lnk_01HPAY...",
"secureUrlHash": "aB3xK9mN2p",
"paymentUrl": "https://switchlock.co.zw/checkout/aB3xK9mN2p",
"status": "PENDING",
"provider": "MOCK_BANK",
"totalAmount": "35000",
"currency": "USD",
"expiresAt": "2026-06-24T09:00:00.000Z"
}The secureUrlHash (10-char random slug) is what you pass as paymentSlug in all downstream endpoints — accept-payment, reject-payment, eft-pay/initiate, etc.
Merchant vs. Developer model
Your company's business_model (set at registration) controls how the seller is resolved.
isMerchant: trueYou are the seller. Every payment link routes funds to your company's merchant avatar user. The sellerId field in the request is ignored — the system resolves the seller automatically.
isMerchant: falseBuyers and sellers are separate registered users in your company. You must pass a valid sellerId on every request — the API returns 400 if it is missing or belongs to a different company.
Link status reference
| Status | Meaning | Terminal? |
|---|---|---|
| PENDING | Link created. Awaiting buyer action. accept-payment and reject-payment are both valid. | No |
| PAID | Buyer verified OTP and funds moved into escrow successfully. | Yes |
| EXPIRED | 24 hours elapsed without payment. Link can no longer be accepted. | Yes |
| CANCELLED | Rejected by the buyer via reject-payment, or cancelled by the system. | Yes |
Who pays the IMTT?
Zimbabwe's Intermediated Money Transfer Tax (IMTT) applies to every electronic transfer. SwitchLock deducts it automatically at the point of escrow release. The feePayer defaults to SELLER — the seller receives the amount minus IMTT and any service fees.
The buyer pays the face amount. IMTT, the service fee, and any system fee are deducted from the seller's payout at release. To override this for a specific transaction, provide a custom splitPlan in the request body.
If you provide a splitPlan, the values must sum to the total amount in the request. The API does not validate this automatically — a mismatch will result in an accounting discrepancy.
Buyer checkout flow
After you generate the link, the buyer completes these steps entirely on their device — no API key required at any point.
1. Request Checkout OTP
The buyer enters their WhatsApp number to receive a 6-digit OTP.
curl -X POST "https://api.switchlock.co.zw/v1/payments/request-checkout-otp" \
-H "Content-Type: application/json" \
-d '{
"whatsappNumber": "+263771234567"
}'| Field | Type | Required |
|---|---|---|
| whatsappNumber | string | Yes |
2. Register Checkout User (If New)
If the buyer is not already registered on SwitchLock, they must be registered before the payment can be processed.
curl -X POST "https://api.switchlock.co.zw/v1/payments/register-checkout-user" \
-H "Content-Type: application/json" \
-d '{
"mobileNumber": "+263771234567",
"idNumber": "63-123456-A-00",
"firstName": "Tendai",
"lastName": "Moyo"
}'| Field | Type | Required |
|---|---|---|
| mobileNumber | string | Yes |
| idNumber | string | Yes |
| firstName | string | No |
| lastName | string | No |
3. Accept Payment
The buyer submits the OTP to finalize the transaction. Funds move to escrow and the link status updates to PAID.
curl -X POST "https://api.switchlock.co.zw/v1/payments/accept-payment" \
-H "Content-Type: application/json" \
-d '{
"paymentSlug": "aB3xK9mN2p",
"whatsappNumber": "+263771234567",
"otp": "847201"
}'| Field | Type | Required | Notes |
|---|---|---|---|
| paymentSlug | string | Yes | The secureUrlHash from the link generation |
| whatsappNumber | string | No | Used for OTP verification |
| phoneNumber | string | No | Fallback if whatsappNumber isn't used |
| otp | string | No | The 6-digit code sent to the buyer |
Reject Payment
A buyer can explicitly reject a payment link. This permanently transitions the link to CANCELLED.
curl -X POST "https://api.switchlock.co.zw/v1/payments/reject-payment" \
-H "Content-Type: application/json" \
-d '{
"paymentSlug": "aB3xK9mN2p"
}'| Field | Type | Required |
|---|---|---|
| paymentSlug | string | Yes |