Developer documentation

Three endpoints and one webhook

Issue an invoice, send the payer to checkout, and learn the outcome. Amounts are integer minor units throughout.

Create

POST/api/v1/invoicesIdempotent on your own reference, so a retried billing job cannot bill a period twice.

Read

GET/api/v1/invoices/:idThe authoritative status. Poll this rather than trusting a webhook to arrive.

Void

POST/api/v1/invoices/:id/voidCancels an unpaid invoice. A paid invoice is never voided by API.

Authentication

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

Create an invoice

POST/api/v1/invoices

Idempotent 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.

request
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"
  }'
201 Created
{
  "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.

Read an invoice

GET/api/v1/invoices/:id

This 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.

200 OK
{ "id": "inv_2f7c…", "status": "PAID", "paidAt": "2026-11-02T04:11:55.000Z" }

Void an invoice

POST/api/v1/invoices/:id/void

Cancels an unpaid invoice. A paid invoice is never voided — that would be a refund, which is handled by agreement rather than by API.

Webhooks

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.

signature
x-lmspay-signature: v1,t=1793664715,s=<hex hmac-sha256>

signed message = "v1:" + timestamp + ":" + rawBody
secret         = the webhook secret issued with your platform

Verify against the raw body, before any JSON parsing. Re-serialising an object produces different bytes and will fail against a genuine signature.

SDK

@lmspay/sdk wraps all of the above, including constant-time signature verification. It is published from this service’s repository.

billing/close-period.ts
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 });

Errors

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": { … } } }
CodeHTTPMeaning
UNAUTHORIZED401Missing or unrecognised platform key.
PLATFORM_SUSPENDED403The key is valid but billing for this platform is suspended.
NOT_FOUND404No invoice with that id belongs to this platform.
INVOICE_NOT_OPEN409The invoice is already paid, voided or expired.
AMOUNT_MISMATCH409A confirmation reported an amount other than the one billed.
VALIDATION_FAILED422The request body failed validation; see details.
GATEWAY_UNAVAILABLE503The payment provider could not be reached.