ReconRails Developer Docs
A deterministic, multi-rail reconciliation engine for African payment processors. Connect your MPESA, Mastercard, and Visa settlement files via REST API — no spreadsheets, no manual matching.
Quick Start
Authenticate and run your first reconciliation in under 5 minutes.
Authentication
JWT-based auth with Bearer tokens. All endpoints are secured.
Matching Engine
9-pass deterministic engine — from 4-key exact match to heuristic residual.
API Reference
68 REST endpoints with live Try It Out against the production API.
What is ReconRails?
ReconRails is an API-first financial reconciliation platform. You upload daily settlement files from your payment rails and core banking system, and the platform automatically matches them using a multi-pass engine — surfacing discrepancies, duplicates, and missing entries as typed exception cases for your ops team to resolve.
Supported payment rails
🟢 M-Pesa 🟠 Mastercard 🔵 Visa
Each rail has its own rule set (YAML-defined) with independent matching passes. All rails reconcile against a shared core banking ledger source.
Base URL
https://platform.reconrails.com
Quick Start
From zero to your first reconciliation result in 5 API calls.
Authenticate and get a token
Send your credentials to get a JWT. The token in the response is used for all subsequent requests.
curl -X POST https://platform.reconrails.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@yourcompany.com","password":"yourpassword"}'
# Response
{
"userId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"email": "admin@yourcompany.com",
"role": "ADMIN",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
import requests
resp = requests.post(
"https://platform.reconrails.com/api/auth/login",
json={"email": "admin@yourcompany.com", "password": "yourpassword"}
)
token = resp.json()["token"]
headers = {"Authorization": f"Bearer {token}"}
const resp = await fetch("https://platform.reconrails.com/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "admin@yourcompany.com", password: "yourpassword" })
});
const { token } = await resp.json();
const headers = { Authorization: `Bearer ${token}` };
Upload your settlement files
Upload both sides of the reconciliation — the scheme file and the core banking ledger. Use multipart form data with a source field identifying the file type.
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." # Upload M-Pesa statement curl -X POST https://platform.reconrails.com/api/upload \ -H "Authorization: Bearer $TOKEN" \ -F "file=@mpesa_statement_20260101.csv" \ -F "source=mpesa-statement" \ -F "date=2026-01-01" # Upload core banking ledger curl -X POST https://platform.reconrails.com/api/upload \ -H "Authorization: Bearer $TOKEN" \ -F "file=@core_banking_20260101.csv" \ -F "source=core-ledger" \ -F "date=2026-01-01"
import requests
def upload(token, filepath, source, date):
with open(filepath, "rb") as f:
return requests.post(
"https://platform.reconrails.com/api/upload",
headers={"Authorization": f"Bearer {token}"},
files={"file": f},
data={"source": source, "date": date}
)
upload(token, "mpesa_statement_20260101.csv", "mpesa-statement", "2026-01-01")
upload(token, "core_banking_20260101.csv", "core-ledger", "2026-01-01")
import { createReadStream } from "fs";
import FormData from "form-data";
async function upload(filepath, source, date) {
const form = new FormData();
form.append("file", createReadStream(filepath));
form.append("source", source);
form.append("date", date);
return fetch("https://platform.reconrails.com/api/upload", {
method: "POST",
headers: { Authorization: `Bearer ${token}`, ...form.getHeaders() },
body: form
});
}
Run reconciliation
Trigger the matching engine for a specific rule set and business date. The engine runs all passes and returns a summary immediately.
curl -X POST https://platform.reconrails.com/api/runs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"businessDate":"2026-01-01"}'
# Response — all configured rails run in one call
{
"runId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"matched": 17,
"partial": 3,
"unmatched": 2,
"suspense": 0,
"matchRate": 90.9,
"discrepancyMinor": 6350
}
result = requests.post(
"https://platform.reconrails.com/api/runs",
headers={**headers, "Content-Type": "application/json"},
json={"businessDate": "2026-01-01"}
).json()
print(f"Match rate: {result['matchRate']}%")
const result = await fetch("https://platform.reconrails.com/api/runs", {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ businessDate: "2026-01-01" })
}).then(r => r.json());
console.log(`Match rate: ${result.matchRate}%`);
Fetch the report
Pull the full reconciliation report for the date, including match group breakdowns and exception counts per rail.
curl "https://platform.reconrails.com/api/report?date=2026-01-01" \ -H "Authorization: Bearer $TOKEN"
report = requests.get(
"https://platform.reconrails.com/api/report",
headers=headers,
params={"date": "2026-01-01"}
).json()
const report = await fetch(
"https://platform.reconrails.com/api/report?date=2026-01-01",
{ headers }
).then(r => r.json());
Review exceptions
Exceptions are auto-classified into break cases (FEE_MISMATCH, AMOUNT_MISMATCH, DUPLICATE, MISSING_IN_CBS, etc.). Your ops team resolves them in-platform or via API.
# List open exception cases curl "https://platform.reconrails.com/api/exception-cases?date=2026-01-01&status=OPEN" \ -H "Authorization: Bearer $TOKEN" # Resolve a case curl -X POST "https://platform.reconrails.com/api/exception-cases/{caseId}/resolve" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"resolution":"WRITE_OFF","note":"Amount within rounding threshold"}'
cases = requests.get(
"https://platform.reconrails.com/api/exception-cases",
headers=headers,
params={"date": "2026-01-01", "status": "OPEN"}
).json()
for case in cases["items"]:
print(case["caseType"], case["discrepancyMinor"])
const { items } = await fetch(
"https://platform.reconrails.com/api/exception-cases?date=2026-01-01&status=OPEN",
{ headers }
).then(r => r.json());
items.forEach(c => console.log(c.caseType, c.discrepancyMinor));
Authentication
ReconRails uses short-lived JWT Bearer tokens. Every API request (except POST /api/auth/login) requires a valid token in the Authorization header.
Getting a token
curl -X POST https://platform.reconrails.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"you@company.com","password":"yourpassword"}'
The response contains a token field — a signed JWT valid for 24 hours.
Using the token
Pass the token as a Bearer credential in every request:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Token payload
| Claim | Description |
|---|---|
| sub | User UUID |
| User email address | |
| role | User role (see Roles & Access) |
| tenant | Tenant UUID — all data is scoped to this tenant |
| exp | Expiry (Unix timestamp, 24 h from issue) |
Cookie auth (platform UI only)
The web platform at platform.reconrails.com uses an HttpOnly, Secure, SameSite=Strict cookie set automatically at login. You do not need to manage this cookie manually — it is only relevant for browser sessions.
Error responses
| Status | Meaning |
|---|---|
| 401 | Missing or invalid token — re-authenticate |
| 403 | Valid token, insufficient role for this operation |
The Matching Engine
ReconRails uses a 9-pass deterministic engine. Passes run in order from most to least specific — an entry matched by an earlier pass is never re-evaluated by a later one.
The 9 passes (MPESA ↔ Core Banking)
| Pass | Keys | Tolerance | Auto-clear | Purpose |
|---|---|---|---|---|
| 1. exact_4key | receipt + MSISDN + type | 0 | Yes | Strongest match — C2B, B2C, Pochi |
| 2. exact_3key | receipt + type | 0 | Yes | B2B (no MSISDN) |
| 3. exact_rrn | RRN | 0 | Yes | CBS echoes MPESA receipt as RRN |
| 4. exact_stan | STAN | 0 | Yes | System trace audit number |
| 5. fee_check | receipt + MSISDN | 0 on principal | No | Detects fee discrepancies → PARTIAL |
| 6. exact_receipt | receipt only | 0 | Yes | CBS missing MSISDN/type fallback |
| 7. tolerance | receipt + MSISDN | KES 1.50 | No | Minor rounding differences |
| 8. batch_settlement | settlement_ref | 0 | Yes | One CBS credit covers many payouts |
| 9. heuristic | MSISDN only | KES 500 | No | Last resort → SUSPENSE queue |
How fee_check works
Pass 5 (fee_check) matches entries where principal amounts are identical but fee_minor differs between sides. It creates a PARTIAL group with the fee discrepancy recorded in discrepancy_minor. These always require human review — auto-clear is disabled regardless of how small the fee difference is.
Fee check runs before the receipt-only fallback (Pass 6) to ensure fee discrepancies are surfaced rather than silently matched.
Payment Rails
ReconRails supports three payment rails out of the box. Each rail has a dedicated rule set and expected file format.
🟢 M-Pesa
| Field | Value |
|---|---|
| Rule ID | mpesa_to_ledger |
| Sources | mpesa-statement + core-ledger |
| File format | CSV — M-Pesa daily transaction export |
| Key identifiers | txn_ref (receipt no.), msisdn, txn_type |
| Transaction types | C2B_PAYBILL, C2B_TILL, C2B_MERCHANT, B2C_PAYMENT, B2B_TRANSFER, POCHI_LA_BIASHARA |
🟠 Mastercard
| Field | Value |
|---|---|
| Rule ID | mastercard_to_cbs |
| Sources | mastercard-clearing + core-ledger |
| File format | CSV — Mastercard IPM clearing file (T112/T140) |
| Key identifiers | auth_code (DE38), rrn (DE37), stan (DE11) |
| Transaction types | PURCHASE (T112), CHARGEBACK (T140), REVERSAL |
🔵 Visa
| Field | Value |
|---|---|
| Rule ID | visa_to_cbs |
| Sources | visa-clearing + core-ledger |
| File format | Pipe-delimited — Visa Base II TC57 |
| Key identifiers | auth_code (AUTH_CODE), rrn (RRN), stan (STAN) |
| Transaction types | TC57 (purchase), TC13 (reversal), TC25 (chargeback) |
Shared Core Banking Ledger
All three rails reconcile against the same core-ledger source. Upload your CBS file once per day — entries are matched to whichever rail's rule set owns the relevant auth_code / txn_ref. Unmatched CBS entries (no scheme-side counterpart) surface as MISSING_IN_SCHEME exceptions.
Match States & Break Types
Every entry and match group has a state. Exception cases are auto-classified into typed break cases by the autoClassifyBreaks background process.
Entry states
| State | Meaning |
|---|---|
| UNMATCHED | Not yet matched by any pass |
| MATCHED | Fully matched with high confidence |
| PARTIAL | Paired but discrepancy detected (fee or amount) |
| SUSPENSE | Tentative heuristic match — awaiting human confirmation |
Auto-classified break types
| Break type | When raised |
|---|---|
| AMOUNT_MISMATCH | Receipt matched but principal amounts differ beyond tolerance |
| FEE_MISMATCH | Receipt matched, amount OK, but fee_minor differs |
| MISSING_IN_CBS | Scheme entry with no CBS counterpart |
| MISSING_IN_MPESA | CBS entry with no scheme counterpart |
| DUPLICATE_TRANSACTION | Same receipt_no appears more than once in scheme file |
| DUPLICATE_ARN | Same ARN/RRN appears more than once |
| CHARGEBACK | Entry with txn_type = CHARGEBACK / T140 |
| WRITE_OFF_CANDIDATE | Unmatched amount ≤ KES 1.00 — auto-flagged for write-off |
| PARTIAL_REVERSAL | Partial group involving a REVERSAL entry |
| LATE_PRESENTMENT | Matched but event times differ by > 24 h |
Resolution states
| Resolution | Meaning |
|---|---|
| WRITE_OFF | Amount difference accepted and written off |
| ADJUSTED | CBS or scheme corrected; re-reconciliation pending |
| ESCALATED | Raised to treasury or scheme for resolution |
| REJECTED | Exception dismissed (was not a real discrepancy) |
Roles & Access Control
ReconRails uses role-based access control (RBAC). Roles are hierarchical — each role includes all permissions of the roles below it.
| Role | What they can do |
|---|---|
| AUDITOR | Read-only: view reports, match groups, audit log. Cannot upload or trigger reconciliation. |
| ANALYST | AUDITOR + view exception cases and break details. |
| OPERATOR | ANALYST + upload files, trigger reconciliation. |
| SUPERVISOR | OPERATOR + resolve exception cases, approve write-offs. |
| ADMIN | SUPERVISOR + manage users, configure connectors, view system health. |
| SUPER_ADMIN | ADMIN + manage tenants, issue licenses. Created only by server operator via .env. |
PLATFORM_SUPER_ADMIN_EMAIL environment variable.
Upload & Reconcile
The full daily workflow: upload your settlement files, run the matching engine, and get results — all via API.
File source identifiers
| source value | Rail | Expected format |
|---|---|---|
| mpesa-statement | MPESA | CSV — M-Pesa daily export |
| core-ledger | All rails | CSV — core banking / wallet journal |
| mastercard-clearing | Mastercard | CSV — IPM T112/T140 |
| visa-clearing | Visa | Pipe-delimited — Base II TC57 |
Upload endpoint
POST /api/upload Content-Type: multipart/form-data Fields: file — the settlement file (CSV or pipe-delimited) source — source identifier (see table above) date — business date in YYYY-MM-DD format
409 Conflict without re-ingesting. To re-process, delete the entry via the admin endpoint first.
Rule IDs for reconciliation
| rule | Reconciles |
|---|---|
| mpesa_to_ledger | mpesa-statement ↔ core-ledger |
| mastercard_to_cbs | mastercard-clearing ↔ core-ledger |
| visa_to_cbs | visa-clearing ↔ core-ledger |
Full three-rail workflow
TOKEN="..." DATE="2026-01-01" BASE="https://platform.reconrails.com" # 1. Upload all files curl -X POST $BASE/api/upload -H "Authorization: Bearer $TOKEN" \ -F "file=@mpesa_statement_$DATE.csv" -F "source=mpesa-statement" -F "date=$DATE" curl -X POST $BASE/api/upload -H "Authorization: Bearer $TOKEN" \ -F "file=@core_banking_mpesa_$DATE.csv" -F "source=core-ledger" -F "date=$DATE" curl -X POST $BASE/api/upload -H "Authorization: Bearer $TOKEN" \ -F "file=@mastercard_ipm_$DATE.csv" -F "source=mastercard-clearing" -F "date=$DATE" curl -X POST $BASE/api/upload -H "Authorization: Bearer $TOKEN" \ -F "file=@core_banking_$DATE.csv" -F "source=core-ledger" -F "date=$DATE" curl -X POST $BASE/api/upload -H "Authorization: Bearer $TOKEN" \ -F "file=@visa_tc57_$DATE.csv" -F "source=visa-clearing" -F "date=$DATE" # 2. Run all rails in one call (RULES_PATH controls which rules run) curl -X POST $BASE/api/runs \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"businessDate\":\"$DATE\"}"
Reading Results
After reconciliation, query the dashboard summary, match groups, and individual entries.
Dashboard summary
curl "https://platform.reconrails.com/api/dashboard?date=2026-01-01" \
-H "Authorization: Bearer $TOKEN"
# Response
{
"date": "2026-01-01",
"rails": [
{
"ruleId": "mpesa_to_ledger",
"totalEntries": 22,
"matched": 17,
"partial": 3,
"unmatched": 2,
"suspense": 0,
"matchRate": 90.9,
"discrepancyKES": 63.50
},
{ "ruleId": "mastercard_to_cbs", ... },
{ "ruleId": "visa_to_cbs", ... }
]
}
Match groups
# List all match groups for a date curl "https://platform.reconrails.com/api/match-groups?date=2026-01-01&state=PARTIAL" \ -H "Authorization: Bearer $TOKEN" # Get a specific group with its entries curl "https://platform.reconrails.com/api/match-groups/{groupId}" \ -H "Authorization: Bearer $TOKEN"
Entries (raw transaction lines)
# UNMATCHED MPESA entries for a date
curl "https://platform.reconrails.com/api/entries?date=2026-01-01&source=mpesa-statement&state=UNMATCHED" \
-H "Authorization: Bearer $TOKEN"
Exception Handling
Exceptions are auto-classified into typed break cases. Your ops team resolves them in-platform or via API — with a full audit trail of every action.
List exception cases
curl "https://platform.reconrails.com/api/exception-cases?date=2026-01-01&status=OPEN" \
-H "Authorization: Bearer $TOKEN"
# Filter by break type
curl "https://platform.reconrails.com/api/exception-cases?caseType=FEE_MISMATCH" \
-H "Authorization: Bearer $TOKEN"
Raise a case manually
curl -X POST https://platform.reconrails.com/api/exception-cases \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"groupId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"caseType": "AMOUNT_MISMATCH",
"note": "CBS recorded KES 2500, MPESA shows KES 2499.50"
}'
Resolve a case
Requires SUPERVISOR or above.
curl -X POST "https://platform.reconrails.com/api/exception-cases/{caseId}/resolve" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"resolution": "WRITE_OFF",
"note": "Amount within KES 1.00 write-off policy"
}'
Reports & Exports
Generate summary and detail reports for any reconciliation date. Export to Excel or PDF for audit sign-off.
Reconciliation report
# Full report (all rails) curl "https://platform.reconrails.com/api/report?date=2026-01-01" \ -H "Authorization: Bearer $TOKEN" # Single-rail report curl "https://platform.reconrails.com/api/report?date=2026-01-01&rule=mpesa_to_ledger" \ -H "Authorization: Bearer $TOKEN"
Export
# Download as Excel curl "https://platform.reconrails.com/api/report/export?date=2026-01-01&format=xlsx" \ -H "Authorization: Bearer $TOKEN" \ -o recon_2026-01-01.xlsx # Download as PDF curl "https://platform.reconrails.com/api/report/export?date=2026-01-01&format=pdf" \ -H "Authorization: Bearer $TOKEN" \ -o recon_2026-01-01.pdf
Audit log
# Full audit trail for a date
curl "https://platform.reconrails.com/api/audit-log?date=2026-01-01" \
-H "Authorization: Bearer $TOKEN"
The audit log records every upload, reconciliation run, exception creation, and resolution — with actor, timestamp, and before/after state. Immutable — no entry can be deleted.
API Reference
68 endpoints — click any endpoint then Try it out to make live requests against the production API.
POST /api/auth/login, and all endpoints will be authenticated.