Developers
The Brillrec API.
Send ledger and bank lines from your own systems, tell Brillrec when a period is complete, send trial balances, and follow every reconciliation with signed webhooks.
- Base URL
https://app.brillrec.com/v1- Authentication
Authorization: Bearer rk_live_…- Format
- JSON over HTTPS
- Limits
- 120 requests a minute per key, 5,000 lines and 8 MB per request
- Sandbox
rk_test_keys, same addresses, made-up data- Plan
- Business plan; every paid plan can try it in a sandbox
Get started · Quick start
Up and running in four calls.
Create a key
An administrator opens Settings → API, webhooks & sandbox in Brillrec and creates a key with the permissions it needs. Start with a sandbox key (
rk_test_).Find the account
curl https://app.brillrec.com/v1/accounts \ -H "Authorization: Bearer $BRILLREC_KEY"Send lines
curl -X POST "https://app.brillrec.com/v1/accounts/12/entries?role=bank" \ -H "Authorization: Bearer $BRILLREC_KEY" -H "Content-Type: application/json" \ -d '{"entries": [{"id": "TXN-000981", "date": "2026-09-30", "amount": "-1500.50"}]}'Close the period
When every line for the month is sent, declare the period complete with the closing balance. Brillrec checks the lines add up and starts the reconciliation. Webhooks tell your system when it is ready or signed off.
Get started · All endpoints
Every endpoint at a glance.
/v1/accountsList your bank accounts and their idsentries:writePOST/v1/accounts/{id}/entries?role=ledger|bankSend cash book or bank linesentries:writePOST/v1/accounts/{id}/periodsDeclare a period complete, with its closing balanceentries:writeGET/v1/accounts/{id}/periods?role=ledger|bankList the periods declaredentries:writeGET/v1/runsList reconciliations with status and totalsruns:readGET/v1/runs/{id}One reconciliation’s status and totalsruns:readPOST/v1/trial-balancesSend a month’s trial balancetb:writePOST/v1/ledger-accounts/{code}/entries?side=ledger|otherSend lines for a balance sheet accountentries:writeGET/v1/balance-sheet?period_end=Balance sheet reconciliation status for a monthruns:readGET/v1/bi/{report}Reports for Power BI and Lookerbi:readGET/v1/exposures?period_end=Open differences classed by exposureruns:readGet started · Keys and access
Keys and access.
The API and webhooks are part of the Business plan; every paid plan can also try them in a sandbox. An administrator creates a key under Settings → API, webhooks & sandbox and chooses what it may do. Brillrec shows the key once: store it in your system’s secret store, never in code or emails. Revoke it there at any time.
| Permission | Allows |
|---|---|
entries:write | List bank accounts, send ledger and bank lines (and a balance sheet account’s lines by its code), declare periods complete. |
runs:read | Read reconciliation status and totals, the balance sheet and the exposures. |
tb:write | Send a month’s trial balance. |
Send the key with every request, over HTTPS, to https://app.brillrec.com (or your organisation’s own Brillrec address):
Authorization: Bearer rk_live_1a2b3c4d_…
Live keys start rk_live_; sandbox keys start rk_test_ and only work in the sandbox (keys made before October 2026 start rk_ and keep working). Limits: 120 requests a minute per key (30 in a sandbox), 5,000 lines and 8 MB per request.
Get started · Sandbox
Build against a sandbox.
A sandbox is a separate Brillrec organisation with made-up data, for your developers or your software house to build and test against the API without touching live data. It comes with every paid plan, one per organisation.
- An administrator opens Settings → API, webhooks & sandbox, chooses Create sandbox and confirms that no real customer data will go in it. Brillrec makes Your Company (Sandbox).
- It starts with two bank accounts fed by the API, and last month’s cash-book and bank lines already sent and declared complete, with the usual differences: bank charges, a cheque not yet presented, a lodgement in transit.
- Invite your developers to the sandbox only, from its Users & roles. They never see your live organisation. Your own administrators can open it at any time.
- In the sandbox, create a key: it starts
rk_test_. Use the same addresses as live; the key decides where the lines go. - Generate test lines adds a month of made-up lines to an account whenever you need more.
| Area | In a sandbox |
|---|---|
| Limits | 2,000 lines sent and 2,000 lines reconciled a month, 3 accounts, 200 AI review items a month, 5 people besides your administrators, 30 requests a minute. |
| Switched off | Bank and accounting connections, SFTP, statements by email, daily reconciliations, the close tracker, reminder emails and billing. |
| Downloads | Every sign-off pack, Excel and CSV file is marked SANDBOX - TEST DATA - NOT FOR AUDIT. |
| Clearing | Test data is cleared after 90 days without use (people, keys and webhooks stay), or at any time with Clear test data. |
| Your plan | The sandbox works while your organisation’s plan is active. If it lapses, or the organisation is deleted, the sandbox pauses or is deleted with it. |
Check before going live
- Send a few lines, then send the same batch again: the answer should show them
replaced, not added twice. - Send one bad line on purpose and check it comes back under
rejected. - Delete a line with
"deleted": true. - Declare the period with a closing balance and check
agreesistrue. - Check the reconciliation Brillrec starts for that period: lines, signs and balances.
- Add a webhook to a test address and check your signature check accepts it.
- Then create a live key (
rk_live_) in your live organisation and switch your system over. Nothing from the sandbox is carried across.
Get started · Errors and limits
What each answer means.
| Status | Meaning |
|---|---|
200 | Done. For lines, check rejected. |
400 | The request can’t be used; detail says why (for example an unknown account, a closed account, more than 5,000 lines, or a sandbox’s monthly limit reached). |
401 | The key is missing, wrong or revoked, a test key was used outside its sandbox, or the sandbox is paused because the organisation’s plan isn’t active. |
403 | The key lacks the permission, or the plan doesn’t include the API. |
404 | Not found, or not in your organisation. |
400 (Financial Close Management) | A trial balance without accounts or a month, an account without a code, name or balance, more than 20,000 accounts; lines for a ledger code no account is matched under, or side=other for an account matched debits against credits. |
413 | More than 8 MB in one request: send smaller batches. |
429 | More than 120 requests a minute (30 in a sandbox): wait a minute and retry. |
Reconciliation · Accounts
Find the account id.
GET /v1/accounts
{"accounts": [{"id": 12, "name": "GTBank main account", "bank": "GTBank",
"number": "0123456789", "currency": "NGN"}]}
Then, under Accounts → ⋯ → Data sources, set the account’s ledger side, bank side or both to Sent by your own system (Brillrec API).
Reconciliation · Sending lines
Send lines as they happen.
POST /v1/accounts/12/entries?role=bank
Content-Type: application/json
{"entries": [
{"id": "TXN-000981", "date": "2026-09-30", "amount": "-1500.50",
"reference": "FT2609300981", "description": "NIP transfer to Ada Okafor",
"counterparty": "Ada Okafor", "session_id": "000013260930101500000000000981"},
{"id": "TXN-000982", "date": "2026-09-30T23:30:00Z", "amount": "250000"}
]}
| Field | Meaning |
|---|---|
role | ledger (your cash book) or bank (the bank statement), in the address. |
id (required) | Your own unique id for the line, up to 100 characters. Sending the same id again replaces the line, so a retry never creates a duplicate. |
date (required) | YYYY-MM-DD, or a date and time with a time zone such as 2026-09-30T23:30:00Z, which is placed on your organisation’s day (23:30 UTC on 30 September is 1 October in Lagos). |
amount (required) | In the account’s currency, as a string or number. Positive = money into the bank account, negative = money out, for both roles. |
reference, description, counterparty, session_id | Optional, but they make matching much better. session_id is the NIBSS NIP session id. |
deleted | true removes the line with that id. |
{"added": 1, "replaced": 1, "deleted": 0,
"rejected": [{"index": 4, "id": "TXN-000985", "reason": "date must be YYYY-MM-DD ..."}]}
A line that can’t be read is listed under rejected with the reason; the others are saved. Fix it and send it again.
Reconciliation · Closing a period
Say when a period is complete.
Brillrec can’t tell “nothing happened” from “your system hasn’t sent the lines yet”. Once a period has ended and every line is sent, declare it, ideally with the account’s closing balance:
POST /v1/accounts/12/periods
{"role": "bank", "period_start": "2026-09-01", "period_end": "2026-09-30",
"closing_balance": "500000.00"}
{"role": "bank", "period_end": "2026-09-30", "complete": true, "closing_balance": "500000.00",
"check": {"agrees": true, "previous_closing": "498500.50", "lines_total": "1499.50",
"expected_closing": "500000.00", "difference": "0.00"}}
- The check: the previous closing balance plus the lines you sent must equal your closing balance. If
agreesisfalse, a line is missing or wrong: look atdifference, fix the lines and declare again. - An empty period counts as “no activity” only once it is declared, with a closing balance equal to the last one.
- Once you declare any period for a side, Brillrec waits for each period’s declaration before it loads that period, so a reconciliation never starts on half-sent data. The closing balance also fills in the opening and closing balances.
- A period can’t be declared before it ends. Declaring it again replaces the earlier declaration.
GET /v1/accounts/12/periods?role=banklists them.
Reconciliation · Reconciliation status
Status and totals.
GET /v1/runs?limit=50
GET /v1/runs/341
{"id": 341, "name": "GTBank main account", "kind": "two_file", "status": "open",
"awaiting_sign_off": false, "daily_check": false, "period": ["2026-09-01", "2026-09-30"],
"currency": "NGN", "bank_account_id": 12, "lines": 1355, "matched": 1093, "explained": 4,
"unexplained": 12, "unexplained_value_minor": 1612988, "unexplained_value_kobo": 1612988, "ai_suggestions_pending": 1, ...}
Only status and totals: no lines, names or narrations. currency is the reconciliation’s currency and is always present. unexplained_value_minor is the unexplained value in the smallest unit of that currency (kobo, cents, pesewas and so on). unexplained_value_kobo carries the same value for integrations written earlier; it is deprecated: use unexplained_value_minor.
Financial Close Management · Trial balances
Send a month’s trial balance.
Needs a key with tb:write. The trial balance is filed on the Trial balances page for the month, replacing any earlier one for that month (from a file, a connection, email or the API), and every balance sheet account signed off for the month whose balance changed, or that is no longer on it, reopens.
POST /v1/trial-balances
Content-Type: application/json
{"period_end": "2026-10-31", "source": "Our ERP", "accounts": [
{"code": "1010", "name": "GTBank main", "type": "asset", "balance": "900000.00",
"opening": "800000.00", "debit": "150000.00", "credit": "50000.00"},
{"code": "3100", "name": "Share capital", "type": "equity", "balance": "-900000.00",
"opening": "-800000.00", "debit": "0", "credit": "100000.00"}
]}
{"id": 88, "period_end": "2026-10-31", "accounts": 2,
"totals": {"debit": 90000000, "credit": 90000000, "difference": 0},
"replaced": 1, "reopened": 0, "currency": "NGN", "amounts": "minor"}
| Field | Meaning |
|---|---|
period_end (required) | Any day in the month; the trial balance is dated at the month end. |
accounts (required) | Up to 20,000 accounts. Each needs a code or a name (both is best) and its closing balance, in the organisation’s accounting currency, as a string or number: debit positive, credit negative. |
type | asset, liability, equity, income or expense. Income and expense accounts are left off the Balance sheet page; without a type, the account’s financial statement line decides. |
opening, debit, credit | Optional: the opening balance and the month’s movements. Send them for every account or for none. |
currency | Optional: the trial balance’s currency code, such as NGN or USD. Balances are in the organisation’s accounting currency; if this names another currency, the request is refused with 400 and nothing is filed. |
source, name | Optional: where it came from (shown on the Trial balances page) and the trial balance’s own name. |
In the answer, totals are in the smallest unit of currency ("amounts": "minor"); replaced is how many earlier trial balances for the month were replaced, and reopened how many signed-off accounts reopened. Sending the same trial balance again replaces it and changes nothing else.
Financial Close Management · Balance sheet account lines
Lines for a balance sheet account.
For an account matched line by line on the Balance sheet page (a clearing or suspense account, a loan, intercompany, a control account), send its lines by its ledger code instead of an account id. Needs entries:write. Lines work exactly as in Sending lines, except that the amount is as the ledger shows it: debit positive, credit negative.
POST /v1/ledger-accounts/2400/entries?side=ledger
{"entries": [{"id": "JV-2026-1001", "date": "2026-10-01", "amount": "-5000000.00",
"reference": "LN-001", "description": "Loan drawdown"}]}
{"added": 1, "replaced": 0, "deleted": 0, "rejected": [], "account_id": 31}
| Field | Meaning |
|---|---|
side | ledger (the account’s own lines, the default) or other (the record it’s matched against, such as the lender’s statement or the other company’s ledger; only for an account matched against another record). |
| the code in the address | The account’s ledger code, as on the trial balance. The account must be set to Match first (Financial Close Management › Balance sheet › Set up); otherwise the answer is 400. |
Then, under the matching account’s Data sources, set the ledger side (and the other side) to Sent by your own system (Brillrec API).
Financial Close Management · Balance sheet and exposures
The balance sheet and its exposures.
Both need runs:read. Amounts are in the smallest unit of the organisation’s accounting currency (kobo for naira, cents for dollars): each answer says "amounts": "minor" and gives the currency, also when the month has no trial balance yet.
GET /v1/balance-sheet?period_end=2026-10-31
{"period_end": "2026-10-31", "amounts": "minor", "currency": "NGN",
"counts": {"accounts": 42, "due": 40, "done": 31, "prepared": 4, "to_approve": 2,
"plans_needed": 1, "differences": 3},
"accounts": [{"code": "2400", "name": "Term loan", "section": "Liabilities", "line": "borrowings",
"ledger": -456250000, "supported": -456250000, "difference": 0,
"how": "clearing", "status": "to_approve",
"classes": {"supported": 0, "timing": 0, "adjust_bs": 0, "adjust_pl": 1000000,
"exposure_bs": 0, "exposure_pl": 0},
"action_plan": "current"}]}
| Field | Values |
|---|---|
how | clearing (matched line by line), schedule, subledger, amortisation, zero, explanation, or linked to its own reconciliation: cash, tax, payables, fixed_assets. |
status | open, prepared, reviewed, to_approve, not_due; for linked and matched accounts not_started, linked_open, linked_done. |
classes | The open items’ amounts by class (each item’s size). Unclassed items and unexplained differences count as exposure_pl. |
action_plan | none (not needed), missing (needed, none written), overdue (needs an update), current; empty when the account isn’t due this month. |
GET /v1/exposures?period_end=2026-10-31&show=high
{"period_end": "2026-10-31", "amounts": "minor", "currency": "NGN",
"totals": {"adjust_bs": 0, "adjust_pl": 1000000, "exposure_bs": 0, "exposure_pl": 5000000},
"other_accounts": {"accounts": 3, "items": 4, "amount": 2500000},
"items": [{"key": "diff:2200:2026-10-31", "code": "2200", "date": "2026-10-31", "age_days": 0,
"class": "exposure_pl", "amount": 5000000, "reference": "",
"description": "Difference not explained by the support or the reconciling items",
"high_risk": true, "commentary": [{"question": "What is this?", "answer": null}]}]}
show=all (the default here) lists every account’s items; show=high lists the accounts needing an action plan, with other_accounts as one total for the rest. Bank reconciliations’ open items are included in balance sheet terms. Descriptions and references are your own records’ text.
Reports · Power BI and Looker
Reports for Power BI and Looker.
A key with only the bi:read permission reads flat reports, one row per item, as JSON or CSV (?format=csv). It comes with Professional and above. Amounts are in each account's currency in full units (naira, dollars), with a currency column. At most 60 requests a minute per key.
GET /v1/bi/reconciliations
Authorization: Bearer rk_live_...
{"dataset": "reconciliations", "count": 1,
"rows": [{"reconciliation_id": 341, "name": "GTBank main account", "type": "Bank", "account_id": 12,
"account": "GTBank main account", "one_off": false, "period_start": "2026-09-01",
"period_end": "2026-09-30", "currency": "NGN", "status": "signed_off", "lines": 1355,
"matched": 1093, "explained": 4, "unexplained": 0, "unexplained_value": 0}]}
| Report | One row per |
|---|---|
/v1/bi/reconciliations | Reconciliation: type (Bank, POS and settlements, Wallet, NIP transfers…), account, period, status, lines matched, explained and unexplained, sign-off. |
/v1/bi/open_items | Open item: reconciliation, account, date, side, amount, age in days, explanation, reference, description, counterparty. |
/v1/bi/accounts | Account: type, currency, data sources, frequency, the latest reconciliation and its status. |
/v1/bi/close_tasks | Financial Close task: workflow, checklist, period, status, due date, preparer, reviewer. |
/v1/bi/controls | Control: area, type, frequency, owner, key control, the latest test and its conclusion. |
Tools that can't send a header (Google Sheets' IMPORTDATA for Looker Studio) may put the key in the address, ?key=rk_live_...: only a key whose one permission is bi:read is accepted there. GET /v1/bi lists the reports.
Events and code · Webhooks
Be told when something happens.
Add a webhook under Settings → API & webhooks with an HTTPS address and the events you want: run.ready, run.signed_off, approval.requested, daily_check.done, balance_sheet.signed_off and balance_sheet.reopened (the account code and month). Each event carries ids, status and totals only.
{"id": "evt_3f9a1c7e5b2d8046aa91c3e7", "event": "run.signed_off",
"created": "2026-10-05T10:12:00Z", "data": {"id": 341, ...}}
Check every request’s signature before trusting it. The header is X-Recon-Signature: t=<unix time>,v1=<signature>, where the signature is the hex HMAC-SHA256 of <t>.<raw body> with the webhook’s secret:
import hmac, hashlib, time
def verified(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300: # refuse replays
return False
mac = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, parts["v1"])
Answer with any 2xx status within 10 seconds. A failed delivery is retried after 1, 5 and 30 minutes, then 2 and 6 hours; after 20 failures in a row the webhook is switched off and your administrators are told.
Events and code · Code examples
Copy, then adapt.
curl
curl -X POST "https://app.brillrec.com/v1/accounts/12/entries?role=ledger" \
-H "Authorization: Bearer $BRILLREC_KEY" -H "Content-Type: application/json" \
-d '{"entries": [{"id": "JV-1001", "date": "2026-09-30", "amount": "-1500.50", "description": "Supplier payment"}]}'Python
import os, requests
r = requests.post("https://app.brillrec.com/v1/accounts/12/entries", params={"role": "ledger"},
headers={"Authorization": f"Bearer {os.environ['BRILLREC_KEY']}"},
json={"entries": [{"id": "JV-1001", "date": "2026-09-30", "amount": "-1500.50",
"description": "Supplier payment"}]}, timeout=30)
r.raise_for_status()
print(r.json())JavaScript (Node 18+)
const r = await fetch("https://app.brillrec.com/v1/accounts/12/periods", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.BRILLREC_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ role: "bank", period_end: "2026-09-30", closing_balance: "500000.00" }),
});
console.log(await r.json());Python: send the trial balance, then read the exposures
import os, requests
H = {"Authorization": f"Bearer {os.environ['BRILLREC_KEY']}"}
tb = {"period_end": "2026-10-31", "source": "Our ERP", "accounts": [
{"code": "1010", "name": "GTBank main", "type": "asset", "balance": "900000.00"},
{"code": "3100", "name": "Share capital", "type": "equity", "balance": "-900000.00"}]}
r = requests.post("https://app.brillrec.com/v1/trial-balances", headers=H, json=tb, timeout=60)
r.raise_for_status()
x = requests.get("https://app.brillrec.com/v1/exposures", headers=H,
params={"period_end": "2026-10-31", "show": "high"}, timeout=30).json()
for item in x["items"]:
print(item["code"], item["class"], item["amount"] / 100)Need a hand?
Email hello@brillrec.com with the request you sent and the answer you got (leave the key out), and we will help. Related: Integrations, Access and data handling, Security.