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.

  1. 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_).

  2. Find the account

    curl https://app.brillrec.com/v1/accounts \
      -H "Authorization: Bearer $BRILLREC_KEY"
  3. 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"}]}'
  4. 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.

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

PermissionAllows
entries:writeList bank accounts, send ledger and bank lines (and a balance sheet account’s lines by its code), declare periods complete.
runs:readRead reconciliation status and totals, the balance sheet and the exposures.
tb:writeSend 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.

  1. 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).
  2. 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.
  3. 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.
  4. In the sandbox, create a key: it starts rk_test_. Use the same addresses as live; the key decides where the lines go.
  5. Generate test lines adds a month of made-up lines to an account whenever you need more.
AreaIn a sandbox
Limits2,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 offBank and accounting connections, SFTP, statements by email, daily reconciliations, the close tracker, reminder emails and billing.
DownloadsEvery sign-off pack, Excel and CSV file is marked SANDBOX - TEST DATA - NOT FOR AUDIT.
ClearingTest data is cleared after 90 days without use (people, keys and webhooks stay), or at any time with Clear test data.
Your planThe 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

  1. Send a few lines, then send the same batch again: the answer should show them replaced, not added twice.
  2. Send one bad line on purpose and check it comes back under rejected.
  3. Delete a line with "deleted": true.
  4. Declare the period with a closing balance and check agrees is true.
  5. Check the reconciliation Brillrec starts for that period: lines, signs and balances.
  6. Add a webhook to a test address and check your signature check accepts it.
  7. 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.

StatusMeaning
200Done. For lines, check rejected.
400The 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).
401The 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.
403The key lacks the permission, or the plan doesn’t include the API.
404Not 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.
413More than 8 MB in one request: send smaller batches.
429More 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"}
]}
FieldMeaning
roleledger (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_idOptional, but they make matching much better. session_id is the NIBSS NIP session id.
deletedtrue 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 agrees is false, a line is missing or wrong: look at difference, 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=bank lists 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"}
FieldMeaning
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.
typeasset, 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, creditOptional: the opening balance and the month’s movements. Send them for every account or for none.
currencyOptional: 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, nameOptional: 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}
FieldMeaning
sideledger (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 addressThe 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"}]}
FieldValues
howclearing (matched line by line), schedule, subledger, amortisation, zero, explanation, or linked to its own reconciliation: cash, tax, payables, fixed_assets.
statusopen, prepared, reviewed, to_approve, not_due; for linked and matched accounts not_started, linked_open, linked_done.
classesThe open items’ amounts by class (each item’s size). Unclassed items and unexplained differences count as exposure_pl.
action_plannone (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}]}
ReportOne row per
/v1/bi/reconciliationsReconciliation: type (Bank, POS and settlements, Wallet, NIP transfers…), account, period, status, lines matched, explained and unexplained, sign-off.
/v1/bi/open_itemsOpen item: reconciliation, account, date, side, amount, age in days, explanation, reference, description, counterparty.
/v1/bi/accountsAccount: type, currency, data sources, frequency, the latest reconciliation and its status.
/v1/bi/close_tasksFinancial Close task: workflow, checklist, period, status, due date, preparer, reviewer.
/v1/bi/controlsControl: 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.

Brillrec demonstration

Let’s look at your workflow.

Tell us a little about your organisation. We prepare an email draft for you to review and send.

Prefer to try it yourself? Start your 14-day free trial.

Step 1 of 2 · Your details

Your details
Email us directly