ReconRails | Developer Docs
platform.reconrails.com

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.

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

endpoint
https://platform.reconrails.com
ℹ️ All requests must be sent over HTTPS. HTTP requests are automatically redirected.

Quick Start

From zero to your first reconciliation result in 5 API calls.

💡 Want to explore without writing code? Use the interactive API Reference — every endpoint has a Try It Out button.
1

Authenticate and get a token

Send your credentials to get a JWT. The token in the response is used for all subsequent requests.

bash
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..."
}
python
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}"}
javascript
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}` };
2

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.

bash
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"
python
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")
javascript
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
  });
}
3

Run reconciliation

Trigger the matching engine for a specific rule set and business date. The engine runs all passes and returns a summary immediately.

bash
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
}
python
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']}%")
javascript
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}%`);
4

Fetch the report

Pull the full reconciliation report for the date, including match group breakdowns and exception counts per rail.

bash
curl "https://platform.reconrails.com/api/report?date=2026-01-01" \
  -H "Authorization: Bearer $TOKEN"
python
report = requests.get(
    "https://platform.reconrails.com/api/report",
    headers=headers,
    params={"date": "2026-01-01"}
).json()
javascript
const report = await fetch(
  "https://platform.reconrails.com/api/report?date=2026-01-01",
  { headers }
).then(r => r.json());
5

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.

bash
# 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"}'
python
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"])
javascript
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

bash
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:

bash
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Token payload

ClaimDescription
subUser UUID
emailUser email address
roleUser role (see Roles & Access)
tenantTenant UUID — all data is scoped to this tenant
expExpiry (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.

⚠️ Tokens are scoped to a single tenant. A token from one tenant cannot access data from another tenant.

Error responses

StatusMeaning
401Missing or invalid token — re-authenticate
403Valid 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 engine is idempotent. Running reconciliation twice for the same date produces identical results.

The 9 passes (MPESA ↔ Core Banking)

PassKeysToleranceAuto-clearPurpose
1. exact_4keyreceipt + MSISDN + type0YesStrongest match — C2B, B2C, Pochi
2. exact_3keyreceipt + type0YesB2B (no MSISDN)
3. exact_rrnRRN0YesCBS echoes MPESA receipt as RRN
4. exact_stanSTAN0YesSystem trace audit number
5. fee_checkreceipt + MSISDN0 on principalNoDetects fee discrepancies → PARTIAL
6. exact_receiptreceipt only0YesCBS missing MSISDN/type fallback
7. tolerancereceipt + MSISDNKES 1.50NoMinor rounding differences
8. batch_settlementsettlement_ref0YesOne CBS credit covers many payouts
9. heuristicMSISDN onlyKES 500NoLast 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

FieldValue
Rule IDmpesa_to_ledger
Sourcesmpesa-statement + core-ledger
File formatCSV — M-Pesa daily transaction export
Key identifierstxn_ref (receipt no.), msisdn, txn_type
Transaction typesC2B_PAYBILL, C2B_TILL, C2B_MERCHANT, B2C_PAYMENT, B2B_TRANSFER, POCHI_LA_BIASHARA

🟠 Mastercard

FieldValue
Rule IDmastercard_to_cbs
Sourcesmastercard-clearing + core-ledger
File formatCSV — Mastercard IPM clearing file (T112/T140)
Key identifiersauth_code (DE38), rrn (DE37), stan (DE11)
Transaction typesPURCHASE (T112), CHARGEBACK (T140), REVERSAL

🔵 Visa

FieldValue
Rule IDvisa_to_cbs
Sourcesvisa-clearing + core-ledger
File formatPipe-delimited — Visa Base II TC57
Key identifiersauth_code (AUTH_CODE), rrn (RRN), stan (STAN)
Transaction typesTC57 (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

StateMeaning
UNMATCHEDNot yet matched by any pass
MATCHEDFully matched with high confidence
PARTIALPaired but discrepancy detected (fee or amount)
SUSPENSETentative heuristic match — awaiting human confirmation

Auto-classified break types

Break typeWhen raised
AMOUNT_MISMATCHReceipt matched but principal amounts differ beyond tolerance
FEE_MISMATCHReceipt matched, amount OK, but fee_minor differs
MISSING_IN_CBSScheme entry with no CBS counterpart
MISSING_IN_MPESACBS entry with no scheme counterpart
DUPLICATE_TRANSACTIONSame receipt_no appears more than once in scheme file
DUPLICATE_ARNSame ARN/RRN appears more than once
CHARGEBACKEntry with txn_type = CHARGEBACK / T140
WRITE_OFF_CANDIDATEUnmatched amount ≤ KES 1.00 — auto-flagged for write-off
PARTIAL_REVERSALPartial group involving a REVERSAL entry
LATE_PRESENTMENTMatched but event times differ by > 24 h

Resolution states

ResolutionMeaning
WRITE_OFFAmount difference accepted and written off
ADJUSTEDCBS or scheme corrected; re-reconciliation pending
ESCALATEDRaised to treasury or scheme for resolution
REJECTEDException 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.

RoleWhat they can do
AUDITORRead-only: view reports, match groups, audit log. Cannot upload or trigger reconciliation.
ANALYSTAUDITOR + view exception cases and break details.
OPERATORANALYST + upload files, trigger reconciliation.
SUPERVISOROPERATOR + resolve exception cases, approve write-offs.
ADMINSUPERVISOR + manage users, configure connectors, view system health.
SUPER_ADMINADMIN + manage tenants, issue licenses. Created only by server operator via .env.
⚠️ SUPER_ADMIN cannot be created via the UI or API. It is provisioned by the server operator through the 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 valueRailExpected format
mpesa-statementMPESACSV — M-Pesa daily export
core-ledgerAll railsCSV — core banking / wallet journal
mastercard-clearingMastercardCSV — IPM T112/T140
visa-clearingVisaPipe-delimited — Base II TC57

Upload endpoint

http
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
ℹ️ Uploads are SHA-256 deduplicated. Re-uploading the exact same file (same bytes, same date) returns a 409 Conflict without re-ingesting. To re-process, delete the entry via the admin endpoint first.

Rule IDs for reconciliation

ruleReconciles
mpesa_to_ledgermpesa-statement ↔ core-ledger
mastercard_to_cbsmastercard-clearing ↔ core-ledger
visa_to_cbsvisa-clearing ↔ core-ledger

Full three-rail workflow

bash
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

bash
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

bash
# 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)

bash
# 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

bash
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

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

bash
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

bash
# 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

bash
# 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

bash
# 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.

🔑 Click Authorize (lock icon), paste your Bearer token from POST /api/auth/login, and all endpoints will be authenticated.