Conventions
Rules that hold across every endpoint in the Reference. Read once — the per-endpoint pages assume all of this.
Base URL & tenant
All paths hang off {host}/lokta-lms/api/v1. The platform is multi-tenant:
every request names its tenant with the Tenant-Identifier header
(this instance's default tenant is default). A missing or wrong tenant
header fails the request before authentication is even considered.
Authentication
HTTP Basic with an application user's credentials, on every request — there is no session or token handshake to keep alive.
curl '{host}/lokta-lms/api/v1/loans' \
-u '{username}:{password}' \
-H 'Tenant-Identifier: default'
What a user may call is governed by their roles' permissions
(see Roles & Permissions in the reference); a valid login without the right permission gets
a 403 with the missing permission named in the error body.
Dates & locale
The write contract is explicit about how it should be read: every body that carries a date also carries the format that date is written in.
{
"locale": "en",
"dateFormat": "dd MMMM yyyy",
"expectedDisbursementDate": "19 August 2026"
}
- Requests send
dateFormat+localeand every date string spelled in exactly that format. - Responses return dates as arrays —
[2026, 8, 19]is year, month, day. - Optional fields are omitted, not sent blank. An empty string where a date or number is optional is a validation error, not a no-op.
Commands
State changes are commands, not bare updates: POST to the resource with a
?command= query parameter. The same URL performs different transitions
depending on the command.
POST /v1/loans/{loanId}?command=approve
POST /v1/loans/{loanId}?command=disburse
POST /v1/loans/{loanId}/transactions?command=repayment
POST /v1/loans/{loanId}/transactions/{transactionId}?command=undowriteoff
Command bodies follow the date convention above; the response echoes the affected ids
(resourceId, sometimes subResourceId and a
changes map).
Pagination & sorting
List endpoints take offset and limit
(plus orderBy / sortOrder where the reference
shows them). Paged responses wrap results as:
{ "totalFilteredRecords": 240, "pageItems": [ … ] }
Endpoints without those parameters return the full list. Field filtering with
?fields=a,b,c trims response payloads.
Errors
Failures return a consistent envelope. The outer message summarises; the
errors array names each offending parameter with a stable
globalisation code you can key translations or handling off.
{
"developerMessage": "The request was invalid…",
"httpStatusCode": "400",
"defaultUserMessage": "Validation errors exist.",
"userMessageGlobalisationCode": "validation.msg.validation.errors.exist",
"errors": [
{
"developerMessage": "The parameter `dueDate` is mandatory.",
"defaultUserMessage": "The parameter `dueDate` is mandatory.",
"userMessageGlobalisationCode": "validation.msg.loancharge.dueDate.cannot.be.blank",
"parameterName": "dueDate"
}
]
}
400— validation; fix the named parameters.401— bad credentials;403— missing permission.404— the resource id does not exist on this tenant.409/400domain rules — the body's globalisation code says which rule (e.g. a write-off on an inactive loan).
Idempotency
Money-movement commands accept an Idempotency-Key header.
Retrying the same command with the same key returns the original result instead of posting twice —
use it on repayments and disbursements issued over unreliable links.