Getting started
The API gives read-only JSON access to one business's records. Make a key under Settings → API keys and send it in a header with every request:
curl "https://crediledger.com/api/v1/loans?limit=5" \ -H "Authorization: Bearer clk_your_key_here"
Base address: https://crediledger.com/api/v1. If your HTTP client cannot set Authorization, send the same key as X-API-Key: clk_… instead. A key is never accepted in the address (the query string) or from a cookie.
A key is read-only and covers the whole business, every branch. Only GET is accepted. GET https://crediledger.com/api/v1/me says which business a key belongs to, its currency and its timezone.
Amounts are numbers, and every record that has amounts carries a currency field with the ISO code they are in: the business's own currency, or the currency the payment was recorded in. Rates are fractions (0.03 means 3%). A loan product's rate is a rate for its rate_period; each business chooses whether that rate is adjusted to how often the client repays (rate_basis period) or charged once for each instalment (per_instalment), and every approved application and every loan records the basis it was given. When the rate is for a day or a week and the client repays daily, weekly or every two weeks, the rate is converted by the calendar: 7 days to a week, 14 days to two weeks and 2 weeks to a two-week period. So 7% a week is 1% for each daily instalment, and 1% a day is 7% for each weekly instalment and 14% for each two-weekly instalment. When a month or a year comes into it (a monthly or yearly rate, or a client who repays monthly), the rate is converted through a year: rate for one instalment = the product's rate × the number of its periods in a year ÷ the number of instalments in a year. For this a year counts as 365 days, 52 weeks, 26 two-week periods and 12 months, so one month is 4.3333 weeks, 2.1667 two-week periods or 30.4167 days. Either way the rate is divided in simple proportion, not compounded. Times are ISO 8601 in the business's timezone; dates are YYYY-MM-DD.
Lists and pages
GET /{resource} returns up to limit records (default 50, at most 100). Ask for the next page with page=2, or follow links.next until it is null:
{
"data": [ … ],
"meta": { "page": 1, "limit": 50, "has_more": true },
"links": { "next": "/api/v1/loans?status=running&page=2&limit=50" }
}
GET /{resource}/{id} returns one record as {"data": {…}}.
Errors and limits
| Status | code | Meaning |
|---|---|---|
| 400 | bad_request | A filter value is wrong, for example a date that is not written YYYY-MM-DD. |
| 401 | unauthorized | The key is missing, wrong or revoked. |
| 403 | plan, subscription_suspended | The business's plan does not include the API, or its subscription is not active. |
| 404 | not_found | No such resource or record. |
| 405 | method_not_allowed | Only GET is supported in this version. |
| 429 | rate_limited | More than 120 requests in one minute with one key. Wait for the seconds in Retry-After. |
Errors look like {"error": {"code": "not_found", "message": "No record with that id."}}. Every answer to a valid key carries X-RateLimit-Limit and X-RateLimit-Remaining.
Resources
Filters go in the query string. from and to are dates, updated_since is a date or an ISO 8601 time, and ids are whole numbers.
clients
The business's clients (borrowers and savers). No national ID, date of birth, employer or next of kin.
| Calls | GET /clients, GET /clients/{id} |
|---|---|
| Filters | q, status, branch_id, updated_since |
| Fields | id, client_number, type, name, first_name, last_name, email, phone, status, branch, open_loans, loans_outstanding, currency, created_at, updated_at |
applications
Loan applications, newest first. from / to filter on the date applied. approved_rate is the rate as the approver confirmed it; approved_rate_basis says how it is charged (period: a rate for approved_rate_period, adjusted to the repayment frequency; per_instalment: once for each instalment) and approved_rate_per_instalment is the rate each instalment is charged.
| Calls | GET /applications, GET /applications/{id} |
|---|---|
| Filters | status, client_id, branch_id, from, to |
| Fields | id, application_number, client, product, branch, amount_requested, currency, requested_term, requested_frequency, purpose, application_fee_status, status, approved_amount, approved_rate, approved_installments, approved_frequency, approved_rate_basis, approved_rate_period, approved_rate_per_instalment, applied_at, decided_at, updated_at |
loans
Disbursed loans, newest first. from / to filter on the disbursement date. The single-record call adds the repayment schedule and outstanding penalties. interest_rate is, as it always was, the rate charged on each instalment (to five decimal places); rate_per_instalment is the same rate at full precision, nominal_rate and nominal_rate_period are the rate as approved, and rate_basis says how one became the other (per_instalment: charged once for each instalment; period: the nominal rate adjusted to the repayment frequency). Loans disbursed before the setting existed are per_instalment. disbursement_method is the name of the payment method the loan was paid out by; null means cash with no method named (every loan disbursed before 10 October 2026) or a loan brought in from earlier records.
| Calls | GET /loans, GET /loans/{id} |
|---|---|
| Filters | status, client_id, branch_id, from, to, updated_since |
| Fields | id, loan_number, application_id, client, product, branch, principal, interest_rate, interest_method, rate_basis, nominal_rate, nominal_rate_period, rate_per_instalment, fees_total, total_repayable, outstanding_balance, currency, installments, frequency, disbursed_at, first_due_date, maturity_date, status, created_at, updated_at, disbursement_method; the single-record call adds schedule, penalties_outstanding and penalties_waived |
schedules
Instalments, by due date. List only; filter by loan_id.
| Calls | GET /schedules |
|---|---|
| Filters | loan_id, status, due_from, due_to |
| Fields | id, loan_id, loan_number, installment_no, due_date, principal_due, interest_due, fees_due, total_due, amount_paid, principal_paid, interest_paid, fees_paid, currency, status, paid_at |
repayments
Repayments received, newest first, with how each was split into principal, interest, fees and penalties. verification_code is the code printed on the receipt, which anyone can check at /verify on this site; null when the receipt has none yet. A voided repayment is still listed: status is "voided" (else "valid") and voided_at says when; leave voided records out of your own totals.
| Calls | GET /repayments, GET /repayments/{id} |
|---|---|
| Filters | loan_id, client_id, branch_id, from, to |
| Fields | id, receipt_number, loan, client, branch, amount, currency, principal, interest, fees, penalties, method, reference, balance_after, date, created_at, verification_code, status, voided_at |
savings-accounts
Savings accounts and their balances.
| Calls | GET /savings-accounts, GET /savings-accounts/{id} |
|---|---|
| Filters | status, client_id, branch_id |
| Fields | id, account_number, client, branch, balance, currency, status, opened_at, created_at, updated_at |
savings-transactions
Deposits, withdrawals, interest and charges on savings accounts, newest first. verification_code is the code printed on a deposit receipt; null for anything that is not a deposit. A voided transaction is still listed, with status "voided" and voided_at; the account balance no longer includes it.
| Calls | GET /savings-transactions, GET /savings-transactions/{id} |
|---|---|
| Filters | account_id, type, from, to |
| Fields | id, account, client_id, type, amount, currency, balance_after, reference, method, external_reference, date, created_at, verification_code, status, voided_at |
payments
Other money received: application, registration and membership fees, penalties paid on their own. verification_code is the code printed on the receipt. A voided payment is still listed, with status "voided" and voided_at.
| Calls | GET /payments, GET /payments/{id} |
|---|---|
| Filters | client_id, branch_id, payment_for, from, to |
| Fields | id, reference, payment_for, client, branch, amount, currency, method, external_reference, date, created_at, verification_code, status, voided_at |
expenses
Office expenses, newest first.
| Calls | GET /expenses, GET /expenses/{id} |
|---|---|
| Filters | branch_id, from, to |
| Fields | id, date, category, description, amount, currency, payment_status, receipt_number, vendor, branch, method, created_at, updated_at |
loan-products
The loan products on offer and their terms. default_rate is a rate for rate_period; rate_basis is how the business charges it on applications it approves now (period: adjusted to the repayment frequency; per_instalment: once for each instalment, whatever rate_period says).
| Calls | GET /loan-products, GET /loan-products/{id} |
|---|---|
| Filters | active |
| Fields | id, name, code, description, interest_method, default_rate, rate_period, rate_basis, default_frequency, min_amount, max_amount, currency, min_term, max_term, term_unit, penalty_type, penalty_rate, grace_period_days, required_guarantors, is_active, created_at, updated_at |
branches
The business's branches. List only.
| Calls | GET /branches |
|---|---|
| Filters | none |
| Fields | id, name, code, location, phone, email, is_main, is_active |
penalty-waivers
Late-payment penalties that were waived (forgiven), newest first: one record per waiver decision. amount is what was forgiven; penalties is how many daily penalties it covered; penalties_owed_before and penalties_owed_after are the loan's penalties still to pay around it. A waiver that was reversed is still listed: status is "reversed" and reversed_at says when, and its amount is owed again. from / to filter on the date it was given; status is standing or reversed. Who gave it and why are not returned.
| Calls | GET /penalty-waivers, GET /penalty-waivers/{id} |
|---|---|
| Filters | loan_id, client_id, branch_id, status, from, to |
| Fields | id, waiver_number, loan, client, branch, amount, currency, penalties, penalties_owed_before, penalties_owed_after, loan_status_before, loan_status_after, waived_at, status, reversed_at |
client, branch, product, loan, account and vendor are small objects with the linked record's id and its name or number.
Not available through the API or webhooks: staff details, clients' national ID numbers, dates of birth, employer and next-of-kin details, guarantors and collateral, internal remarks and notes, who brought a payment in, and bank account details.
Webhooks
Webhooks push events to your server instead of you asking. Add an address under Settings → Webhooks and pick the events. Within about a minute of the event we send a POST with a JSON body:
POST /your/path HTTP/1.1
Content-Type: application/json
Crediledger-Event: repayment.received
Crediledger-Delivery: evt_1842
Crediledger-Signature: t=<unix time>,v1=<64 hex characters>
{
"id": "evt_1842",
"type": "repayment.received",
"created_at": "<ISO 8601 time>",
"business": { "id": <number>, "name": "<the business's name>" },
"data": { … the same fields as GET /api/v1/repayments/{id} … }
}
The address must be https and reachable from the internet; addresses inside a private network, on this server or on a local machine are refused, and redirects are not followed.
Answer with any 2xx status within 10 seconds; do slow work after answering. Otherwise we try again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours, then give up. After 50 failures in a row the webhook is switched off. Every retry carries the same body and the same id. An event can occasionally arrive twice: use id to ignore repeats.
Checking the signature
Crediledger-Signature holds a time (t) and v1, the HMAC-SHA256 of t + "." + the raw body keyed with the webhook's signing secret (whsec_…, shown once when you add the webhook or make a new secret). Compute it yourself over the bytes exactly as received and compare; also refuse messages more than 5 minutes old.
// PHP
$body = file_get_contents('php://input');
$secret = getenv('CREDILEDGER_WEBHOOK_SECRET'); // whsec_…
preg_match('/t=(\d+),v1=([0-9a-f]{64})/', $_SERVER['HTTP_CREDILEDGER_SIGNATURE'] ?? '', $m);
$ok = $m && abs(time() - (int) $m[1]) < 300
&& hash_equals(hash_hmac('sha256', $m[1] . '.' . $body, $secret), $m[2]);
if (!$ok) { http_response_code(400); exit; }
$event = json_decode($body, true);
http_response_code(200);
// Node.js (Express: use express.raw({ type: 'application/json' }) for this route)
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const m = /t=(\d+),v1=([0-9a-f]{64})/.exec(header || '');
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(m[1] + '.' + rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}
// verify(req.body, req.get('Crediledger-Signature'), process.env.CREDILEDGER_WEBHOOK_SECRET)
Events
| Event | When | data is |
|---|---|---|
client.created | A client is registered | a client: the fields of clients |
application.submitted | A loan application is received | an application: the fields of applications |
application.approved | A loan application is approved | an application: the fields of applications |
application.declined | A loan application is declined | an application: the fields of applications |
loan.disbursed | A loan is paid out | a loan, with its schedule: the fields of loans |
loan.settled | A loan is fully repaid and closed | a loan, with its schedule: the fields of loans |
loan.defaulted | A loan is marked as in default | a loan, with its schedule: the fields of loans |
repayment.received | A repayment is recorded | a repayment: the fields of repayments |
repayment.voided | A repayment is voided (taken back off the loan) | a repayment: the fields of repayments |
savings.transaction_posted | A savings deposit, withdrawal, interest or charge is posted | a savings transaction: the fields of savings-transactions |
savings.transaction_voided | A savings deposit or withdrawal is voided | a savings transaction: the fields of savings-transactions |
payment.received | Another payment is received (application, registration or membership fee, penalty) | a payment: the fields of payments |
payment.voided | Another payment is voided | a payment: the fields of payments |
expense.recorded | An expense is recorded | an expense: the fields of expenses |
penalty.waived | Late-payment penalties on a loan are waived (forgiven) | a penalty waiver: the fields of penalty-waivers |
penalty.waiver_reversed | A penalty waiver is reversed (the penalties are owed again) | a penalty waiver: the fields of penalty-waivers |
ping | You press "Send a test event" | {"message": "…"} |
data shows the record as it is when the message is first sent, in the same shape as the API's single-record call.
Keys and webhooks are managed by the business's owner or an admin after signing in, under Settings. Questions: contact the team.