Payments.lk

For developers and AI agents

One endpoint. Keys on approval.

A REST API whose hosted checkout runs 3D Secure on Payable’s payment page, so a card number never touches your server or ours. Sandbox keys arrive with your application, sandbox checkouts once you submit it, so you can integrate while underwriting runs, and you create live keys in your dashboard once you are approved. If you would rather not write any of it, payment links need no code at all.

Take your first payment
curl https://api.payments.lk/v1/checkouts \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: order-8891" \
-H "Content-Type: application/json" \
-d '{"amountCents": 350000, "description": "Order 8891"}'

Predictable by design

One resource, checkouts, with the amount in LKR and a return URL. Idempotency keys on every write, so a retried request never charges twice. Every error is a sentence, with a reason your code can match and a link that explains it.

Sandbox from day one

Test keys are issued the moment you start setup, before anyone has read your documents. Once you submit your application, Payable's sandbox sets up a test merchant for you, and sandbox payments run there with the card networks' test cards, so approvals, declines and 3D Secure behave as they will in production.

Readable by machines

The pricing, the onboarding steps, the API shape and the comparison against every competitor are published in plain text at /llms.txt and as structured data on every page, so an agent asked to pick a Sri Lankan gateway can check the arithmetic itself.

AI agents

The gateway an agent can read and run.

Ask Claude or ChatGPT for the cheapest way to take card payments in Sri Lanka and it should be able to check the answer. So every rate, fee, cap and step is published as plain text and structured data, with dates and sources. And a merchant can hand an agent a key of its own: an MCP server at api.payments.lk/mcp lets it list and explain payments, summarise a day, create payment links and prepare refunds that the merchant approves in the dashboard before anything moves. It works in Claude Code, Cursor, VS Code, Codex and the Claude API today. Payouts and disputes follow when those feeds are connected.

/llms.txt
A short summary: what it is, what it costs, how to apply.
/llms-full.txt
The whole fact sheet and every FAQ answer as one text document.
/facts
The fact sheet as tables with anchors, for people and for quoting.
/faq
Every question a founder asks, answered in full, with FAQPage data.
/sitemap.xml
Everything indexable, with dates.

The API

JSON over HTTPS with your key as a bearer token. A sandbox key (sk_test_) works on sandbox rows only and a live key (sk_live_) on live rows only. Every write needs an Idempotency-Key: send the same key again within 24 hours and you get the first answer back, never a second payment, and test and live keys keep their keys apart. Amounts are whole cents of LKR: Rs. 3,500 is 350000.

Every error carries a code, a reason and a docUrl. What every error means, and how to fix it.

POST /v1/checkouts
A payment and the hosted page that takes it. Send amountCents and a description, or lineItems for the total to be worked out from them, and optionally reference, customer, successUrl, cancelUrl, saveCard and locale. Answers with url: send the customer there. In an app, set the return URLs to your app’s own scheme, such as myshop://paid.
GET /v1/checkouts/:id
A checkout and its payment.
GET /v1/payments
Your payments, newest first. Filter by status; page with limit and cursor.
GET /v1/payments/:id
One payment.
POST /v1/refunds
Refunds a payment in full or in part. A live refund goes out once the payment has settled, the next bank working day.
GET /v1/refunds
Your refunds, newest first. Filter by paymentId; page with limit and cursor.
GET /v1/refunds/:id
One refund.
POST /v1/payment_links
A fixed amount link you can send anywhere. Each payer gets a payment of their own.
GET /v1/payment_links
Your links, with how many payments each has taken.
DELETE /v1/payment_links/:id
Turns a link off.
GET /v1/cards
Cards your customers kept on file.
POST /v1/cards/:id/charge
Takes a payment on a saved card, without the customer present. Secret key only.
DELETE /v1/cards/:id
Deletes a saved card at the processor.
GET /v1/events
Your events, newest first, in the shape your webhooks receive. Filter by type and since; page with limit and cursor.
GET /v1/events/:id
One event.
GET /v1/account
Your account: status, plan, this month's use of the monthly limit and whether live payments are open.

Every endpoint, with its parameters, its errors and a request in curl, Node.js and PHP, is in the API reference. Tools can read the same thing as an OpenAPI 3.1 document.

Create a checkout

curl https://api.payments.lk/v1/checkouts \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: order-8891" \
  -H "Content-Type: application/json" \
  -d '{
    "amountCents": 350000,
    "description": "Two kottu and a milk tea",
    "reference": "order-8891",
    "successUrl": "https://yourshop.lk/thanks",
    "cancelUrl": "https://yourshop.lk/cart"
  }'

Send your customer to the url in the answer. When they have paid you receive payment.succeeded; the successUrl is only a way back for their browser, so fulfil the order from the webhook, not from the redirect. The Node.js and PHP SDKs do both for you.

Verify a webhook

import { createHmac, timingSafeEqual } from "node:crypto";

// header: the Payments-Signature header, "t=1726400000,v1=<hex>". While a rotated
//         signing secret is kept it carries a v1 for each secret: accept any match.
// body:   the raw request body, exactly as received
export function verify(secret, header, body, toleranceSeconds = 300) {
  let t;
  const signatures = [];
  for (const part of String(header ?? "").split(",")) {
    const [key, value] = part.trim().split("=", 2);
    if (key === "t" && /^[0-9]{1,12}$/.test(value ?? "")) t = Number(value);
    if (key === "v1" && /^[0-9a-f]{64}$/.test(value ?? "")) signatures.push(value);
  }
  if (t === undefined || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(t + "." + body).digest();
  return signatures.some((s) => timingSafeEqual(expected, Buffer.from(s, "hex")));
}

Sandbox

A sandbox checkout is a real checkout on Payable’s sandbox: the customer lands on Payable’s payment page, pays with one of these test cards, and the outcome comes back as a signed notification and webhook, exactly as it will live. Use CVV 100 (1000 for American Express); the expiry date decides the answer, and on the 3D Secure page choose an authentication result. No money moves. Sandbox checkouts start once you have submitted your application, when Payable’s sandbox has a test merchant for you.

Follow the developer guide, sandbox first
5123 4500 0000 0008
Mastercard
2223 0000 0000 0007
Mastercard
4508 7500 1574 1019
Visa
3718 812455 60002
American Express, CVV 1000
3600 0000 0000 0123
Diners Club
6445 6445 6445 6460
Discover
Expiry 01/39
Approved
Expiry 05/39
Declined
Expiry 04/27
Expired card

Webhooks

Signed over the exact bytes sent, with the time inside the signature, and retried at 1, 5 and 30 minutes, then 2, 6 and 12 hours. Add an endpoint, or point events at the test inbox on your dashboard before you have a server.

payment.succeeded
The card was authorised. The payment object carries the amount, the fee and the card's last four digits.
payment.failed
The card was declined or the payment was not completed.
checkout.expired
Nobody paid the checkout before it expired, 30 minutes after it was made.
card.saved
The customer kept their card on file. Charge it later with POST /v1/cards/:id/charge.
card.save_failed
The payment succeeded but the card was not kept, so no card.saved follows. The payment's cardSave is failed; ask the customer to save the card again.
refund.succeeded
The money is on its way back to the customer’s card.
refund.failed
The refund could not be made. The refund object says why.
test.ping
Sent only when you press Send test event on an endpoint's page in the dashboard, to that endpoint alone. It carries no payment; answer 2xx and do nothing else.
Developers · Payments.lk