For developers

API and webhooks

Connect a Crediledger business to a website, an accounting tool or anything else: read its records through the REST API, and be told the moment something happens through webhooks.

Version 1 · read-only API: Premium and Enterprise Webhooks: Standard, Premium and Enterprise

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

StatuscodeMeaning
400bad_requestA filter value is wrong, for example a date that is not written YYYY-MM-DD.
401unauthorizedThe key is missing, wrong or revoked.
403plan, subscription_suspendedThe business's plan does not include the API, or its subscription is not active.
404not_foundNo such resource or record.
405method_not_allowedOnly GET is supported in this version.
429rate_limitedMore 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.

CallsGET /clients, GET /clients/{id}
Filtersq, status, branch_id, updated_since
Fieldsid, 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.

CallsGET /applications, GET /applications/{id}
Filtersstatus, client_id, branch_id, from, to
Fieldsid, 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.

CallsGET /loans, GET /loans/{id}
Filtersstatus, client_id, branch_id, from, to, updated_since
Fieldsid, 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.

CallsGET /schedules
Filtersloan_id, status, due_from, due_to
Fieldsid, 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.

CallsGET /repayments, GET /repayments/{id}
Filtersloan_id, client_id, branch_id, from, to
Fieldsid, 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.

CallsGET /savings-accounts, GET /savings-accounts/{id}
Filtersstatus, client_id, branch_id
Fieldsid, 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.

CallsGET /savings-transactions, GET /savings-transactions/{id}
Filtersaccount_id, type, from, to
Fieldsid, 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.

CallsGET /payments, GET /payments/{id}
Filtersclient_id, branch_id, payment_for, from, to
Fieldsid, reference, payment_for, client, branch, amount, currency, method, external_reference, date, created_at, verification_code, status, voided_at

expenses

Office expenses, newest first.

CallsGET /expenses, GET /expenses/{id}
Filtersbranch_id, from, to
Fieldsid, 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).

CallsGET /loan-products, GET /loan-products/{id}
Filtersactive
Fieldsid, 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.

CallsGET /branches
Filtersnone
Fieldsid, 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.

CallsGET /penalty-waivers, GET /penalty-waivers/{id}
Filtersloan_id, client_id, branch_id, status, from, to
Fieldsid, 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

EventWhendata is
client.createdA client is registereda client: the fields of clients
application.submittedA loan application is receivedan application: the fields of applications
application.approvedA loan application is approvedan application: the fields of applications
application.declinedA loan application is declinedan application: the fields of applications
loan.disbursedA loan is paid outa loan, with its schedule: the fields of loans
loan.settledA loan is fully repaid and closeda loan, with its schedule: the fields of loans
loan.defaultedA loan is marked as in defaulta loan, with its schedule: the fields of loans
repayment.receivedA repayment is recordeda repayment: the fields of repayments
repayment.voidedA repayment is voided (taken back off the loan)a repayment: the fields of repayments
savings.transaction_postedA savings deposit, withdrawal, interest or charge is posteda savings transaction: the fields of savings-transactions
savings.transaction_voidedA savings deposit or withdrawal is voideda savings transaction: the fields of savings-transactions
payment.receivedAnother payment is received (application, registration or membership fee, penalty)a payment: the fields of payments
payment.voidedAnother payment is voideda payment: the fields of payments
expense.recordedAn expense is recordedan expense: the fields of expenses
penalty.waivedLate-payment penalties on a loan are waived (forgiven)a penalty waiver: the fields of penalty-waivers
penalty.waiver_reversedA penalty waiver is reversed (the penalties are owed again)a penalty waiver: the fields of penalty-waivers
pingYou 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.