Create
/api/v1/invoicesIdempotent on your own reference, so a retried billing job cannot bill a period twice.Issue an invoice, send the payer to checkout, and learn the outcome. Amounts are integer minor units throughout.
/api/v1/invoicesIdempotent on your own reference, so a retried billing job cannot bill a period twice./api/v1/invoices/:idThe authoritative status. Poll this rather than trusting a webhook to arrive./api/v1/invoices/:id/voidCancels an unpaid invoice. A paid invoice is never voided by API.Every API request carries your platform key as a bearer token. Keys are issued in the operator console and shown once; we store only a hash, so a lost key is rotated rather than recovered.
Authorization: Bearer lmspay_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/api/v1/invoicesIdempotent on externalRef: send the same reference twice and you get the same invoice back, with 200 instead of 201. Use your own billing-period id as the reference and a retry can never bill the same period twice.
curl -X POST https://lmspay.online/api/v1/invoices \
-H "Authorization: Bearer $LMSPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalRef": "period_2026_10",
"periodKey": "2026-10",
"amountMinor": 4500000,
"currency": "LKR",
"description": "Platform fee — October 2026",
"dueAt": "2026-11-08T00:00:00.000Z"
}'{
"id": "inv_2f7c…",
"status": "OPEN",
"amountMinor": 4500000,
"currency": "LKR",
"checkoutUrl": "https://lmspay.online/i/inv_2f7c…"
}Amounts are integer minor units — 4500000 is LKR 45,000.00.
/api/v1/invoices/:idThis is the authoritative status. Treat it as the source of truth, not the webhook. A webhook that never arrives is silent; a scheduled poll closes that gap without depending on our delivery.
{ "id": "inv_2f7c…", "status": "PAID", "paidAt": "2026-11-02T04:11:55.000Z" }/api/v1/invoices/:id/voidCancels an unpaid invoice. A paid invoice is never voided — that would be a refund, which is handled by agreement rather than by API.
We POST invoice.paid, invoice.voided and invoice.expired to your endpoint. Answer with any 2xx. Anything else is retried nine times over about a day, then abandoned and shown in our console for manual replay.
Each request carries x-lmspay-signature. The timestamp is inside the signed material, so an old capture stops verifying — reject anything more than five minutes out.
x-lmspay-signature: v1,t=1793664715,s=<hex hmac-sha256>
signed message = "v1:" + timestamp + ":" + rawBody
secret = the webhook secret issued with your platformVerify against the raw body, before any JSON parsing. Re-serialising an object produces different bytes and will fail against a genuine signature.
@lmspay/sdk wraps all of the above, including constant-time signature verification. It is published from this service’s repository.
import { LmsPay, verifyWebhook, SIGNATURE_HEADER } from '@lmspay/sdk';
const pay = new LmsPay({ baseUrl: process.env.LMSPAY_URL, apiKey: process.env.LMSPAY_API_KEY });
const invoice = await pay.invoices.create({
externalRef: period.id,
amountMinor: period.feeAmountMinor,
currency: 'LKR',
description: `Platform fee — ${period.periodKey}`,
});
// -> invoice.checkoutUrl
// in your webhook route — read the RAW body, before any JSON parsing
const rawBody = await request.text();
const ok = verifyWebhook({
rawBody,
header: request.headers.get(SIGNATURE_HEADER),
secret: process.env.LMSPAY_WEBHOOK_SECRET,
});
if (!ok) return new Response('bad signature', { status: 400 });Every failure returns the same shape, with a stable machine-readable code. Branch on the code, never on the message — the wording will be improved.
{ "error": { "code": "VALIDATION_FAILED", "message": "Invalid invoice.", "details": { … } } }| Code | HTTP | Meaning |
|---|---|---|
| UNAUTHORIZED | 401 | Missing or unrecognised platform key. |
| PLATFORM_SUSPENDED | 403 | The key is valid but billing for this platform is suspended. |
| NOT_FOUND | 404 | No invoice with that id belongs to this platform. |
| INVOICE_NOT_OPEN | 409 | The invoice is already paid, voided or expired. |
| AMOUNT_MISMATCH | 409 | A confirmation reported an amount other than the one billed. |
| VALIDATION_FAILED | 422 | The request body failed validation; see details. |
| GATEWAY_UNAVAILABLE | 503 | The payment provider could not be reached. |