Payment Links
Send one customer a hosted checkout URL for a known amount.
A payment link is a hosted checkout URL created for one customer and one amount. Your backend already knows who is paying, what they owe, and in which currency.
Use it for invoices, payment requests, and order checkouts where you send the customer somewhere to pay rather than redirecting them from your own flow.
| Use instead | If |
|---|---|
| Transactions | The customer is already in your checkout flow and you can redirect. |
| General Payment Links | The link is reusable and the customer is unknown when you create it. |
| Virtual Accounts | You want a bank transfer rather than a checkout page. |
The Flow
sequenceDiagram
participant Y as Your backend
participant K as Kyshi
participant C as Customer
Y->>K: POST /v1/pay/initialize
K-->>Y: paymentLink.url + code
Y->>C: Send the link
C->>K: Pays on the hosted page
K-->>Y: Webhook charge.success
Y->>K: GET /v1/pay/verify-api/{code}
K-->>Y: status
Y->>C: Fulfil (only if SUCCESS)
Build It
1. Create the link
-H "x-api-key: your_secret_key"POST {{host}}/v1/pay/initializeInclude the customer, the amount and currency, and your own reference so the payment can be traced back to the invoice it settles.
Create links server-side. A link created in the browser is a link whose amount the customer can change.
2. Send the link
Deliver paymentLink.url however you normally reach the customer — email, WhatsApp, your own dashboard.
3. Verify before fulfilling
GET {{host}}/v1/pay/verify-api/{code}Act on the charge.success webhook, then confirm with verification before releasing anything. The customer opening the link is not payment; OPENED is an engagement signal only.
Statuses
| Status | Meaning |
|---|---|
PENDING | Created, not yet paid. |
OPENED | Customer opened the link. Not a payment. |
SUCCESS | Paid. |
FAILED | Attempted and failed. |
COLLECTED | Funds collected against the link. |
When It Fails
Treating OPENED as paid
OPENED as paidIt means someone loaded the page. Nothing more.
The link outlives the price
A link created today may be paid next week. If it is priced in settlement currency, the local amount is recalculated at payment time and the customer may see a different figure from the one you quoted.
For links with a long life, price in local so the amount cannot drift, or make clear the final amount is set when they pay. See FX And Rates.
The customer pays twice
Send a link twice and some customers pay twice. Check whether the link is already SUCCESS before re-sending, and make fulfilment idempotent on your order ID.
Nobody pays
Links go stale. Track unpaid links and chase or expire them rather than letting them accumulate.
Reconcile
async function reconcilePaymentLinks() {
for (const link of await db.paymentLinks.unpaidOlderThan({ minutes: 30 })) {
const remote = await kyshi.paymentLinks.verify(link.code);
if (remote.status === 'SUCCESS') await fulfilOnce(link.orderId);
}
}Test It
- A link is paid and fulfils exactly once.
- The same webhook twice still fulfils once.
- An opened-but-unpaid link does not fulfil.
- A failed payment leaves the link payable.
Next
- Initialise Payment Link · Verify Payment Link
- General Payment Links — the reusable variant
Updated 5 days ago
