Fedha API
A multi-tenant, double-entry accounting engine. Any system that needs accounting — rental management, transport, e-commerce, schools — can plug in via HTTP. You post transactions, Fedha handles the ledger math and produces financial reports.
Core Concepts
Multi-tenancy
Every system that uses Fedha API is a tenant. A tenant's data is completely isolated — one tenant cannot see another's accounts, journals, or reports. You get one API key per tenant and include it on every request.
Your App ──→ POST /journal (X-API-Key: your-key) ──→ Fedha API ──→ Neon DB
Double-entry bookkeeping
Every financial event is recorded as a journal entry with two or more lines. Total debits must always equal total credits. The API enforces this — it rejects any unbalanced request.
Example: Customer pays rent of 500,000 TZS
DR Bank Account (1020) 500,000
CR Rent Receivable (1110) 500,000
Idempotency
If you post a journal entry with a source_type and source_id, the API remembers it. Posting the same source twice is safe — the old entry is automatically reversed and replaced. Retry on network failure without creating duplicates.
POST /journal {source_type: "payment", source_id: "pay_123"} → creates entry
POST /journal {source_type: "payment", source_id: "pay_123"} → reverses old, creates new
System Labels
Instead of memorising account codes, reference accounts by a label like PRIMARY_BANK or AR_CONTROL. Labels are consistent across all tenants that seeded the same domain, so your integration code is portable.
Authentication
Fedha uses two types of API keys. Keep them separate — the admin key provisions tenants, the tenant key does everything else.
Tenant API Key — for all accounting operations
Used for posting journals, fetching reports, and reconciliation.
X-API-Key: your-tenant-api-key
Admin Key — for provisioning only
Used only when creating tenants or API keys. This key stays on your server and is never shared with client apps.
X-Admin-Key: your-admin-secret-key
X-Admin-Key in frontend code or mobile apps. Only use it in server-side scripts or admin tooling.Quick Start
From zero to posting your first transaction in 4 steps.
Create a tenant
This creates an isolated accounting namespace for your application.
curl -X POST https://fedha-api.com/admin/tenants \
-H "X-Admin-Key: YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "My App", "currency": "TZS"}'
Save the id from the response — you need it next.
Create an API key
curl -X POST https://fedha-api.com/admin/tenants/{tenant_id}/keys \
-H "X-Admin-Key: YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "production"}'
key value is only shown once at creation. Store it immediately in your environment variables as FEDHA_API_KEY.Seed a Chart of Accounts
Pick the domain that matches your business:
curl -X POST "https://fedha-api.com/accounts/seed?domain=rental" \
-H "X-API-Key: fapi_xxxxxxxxxxxxxxxx"
Safe to call multiple times — it skips existing account codes.
Post your first journal entry
curl -X POST https://fedha-api.com/journal \
-H "X-API-Key: fapi_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"date": "2026-05-20",
"description": "Rent payment from John Doe",
"source_type": "payment",
"source_id": "pay_001",
"lines": [
{"account_label": "PRIMARY_BANK", "debit": 500000, "credit": 0},
{"account_label": "AR_CONTROL", "debit": 0, "credit": 500000}
]
}'
🎉 You're now recording accounting transactions. Head to the report endpoints to see the numbers.
Admin — Tenants & API Keys
All admin endpoints require the X-Admin-Key header.
Create a tenant
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Display name for the tenant |
currency | string | no | Default "TZS". Any 3-letter currency code. |
List all tenants
Get a single tenant
Deactivate a tenant
Sets is_active: false. Does not delete data.
Create an API key
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Label for this key, e.g. "production", "staging" |
List API keys for a tenant
Revoke an API key
Sets is_active: false. Requests using this key will immediately receive 401.
Chart of Accounts
All endpoints require X-API-Key header.
Seed a full Chart of Accounts
| Param | Values | Description |
|---|---|---|
domain | rental | transport | generic | Which account template to load |
Create a custom account
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | Unique numeric code e.g. "6010" |
name | string | yes | Human-readable name |
type | string | yes | asset | liability | equity | revenue | expense |
parent_code | string | no | Code of the parent account |
system_label | string | no | Your own label for journal lines |
List accounts
| Param | Type | Default | Description |
|---|---|---|---|
account_type | string | — | Filter by type |
active_only | boolean | true | Set false to include deactivated |
Get / Update an account
Rental Domain — Full Account List
| Code | Name | Type | Label |
|---|---|---|---|
1010 | Cash on Hand | asset | CASH_ON_HAND |
1020 | Bank Account | asset | PRIMARY_BANK |
1025 | Mobile Money — M-Pesa | asset | MPESA |
1026 | Mobile Money — Tigo Pesa | asset | TIGOPESA |
1027 | Mobile Money — HaloPesa | asset | HALOPESA |
1028 | Mobile Money — Airtel Money | asset | AIRTEL_MONEY |
1030 | Petty Cash | asset | PETTY_CASH |
1110 | Rent Receivable | asset | AR_CONTROL |
1120 | Security Deposits Held | asset | SECURITY_DEPOSIT_ASSET |
1130 | Input VAT Recoverable | asset | INPUT_VAT |
1310 | Buildings & Improvements | asset | — |
1320 | Furniture & Fittings | asset | — |
1390 | Accumulated Depreciation | asset | ACCUM_DEPRECIATION |
2010 | Accounts Payable | liability | AP_CONTROL |
2020 | Security Deposits Payable | liability | SECURITY_DEPOSIT_LIABILITY |
2030 | VAT Payable | liability | VAT_PAYABLE |
2040 | Accrued Expenses | liability | ACCRUED_EXPENSES |
2050 | Advance Rent Received | liability | ADVANCE_RENT |
2110 | Loans Payable | liability | LOANS_PAYABLE |
3010 | Owner's Capital | equity | OWNER_CAPITAL |
3020 | Retained Earnings | equity | RETAINED_EARNINGS |
3030 | Reversal Clearing | equity | REVERSAL_CLEARING |
4010 | Rent Revenue | revenue | RENT_REVENUE |
4020 | Late Fee Revenue | revenue | LATE_FEE_REVENUE |
4030 | Service Charge Revenue | revenue | SERVICE_CHARGE_REVENUE |
4040 | Other Revenue | revenue | OTHER_REVENUE |
5010 | Property Maintenance | expense | MAINTENANCE_EXPENSE |
5020 | Utilities — Common Areas | expense | UTILITIES_EXPENSE |
5030 | Insurance | expense | INSURANCE_EXPENSE |
5040 | Property Tax & Rates | expense | PROPERTY_TAX_EXPENSE |
5050 | Management Fees | expense | MANAGEMENT_FEE_EXPENSE |
5060 | Salaries & Wages | expense | SALARY_EXPENSE |
5070 | Advertising & Marketing | expense | ADVERTISING_EXPENSE |
5080 | Legal & Professional Fees | expense | LEGAL_EXPENSE |
5090 | Depreciation | expense | DEPRECIATION_EXPENSE |
5100 | Office & Admin | expense | ADMIN_EXPENSE |
5190 | Miscellaneous Expenses | expense | MISC_EXPENSE |
Journal Entries
The core of Fedha API. Every financial event — a payment, an expense, an invoice — becomes a journal entry.
Post a journal entry
| Field | Type | Required | Description |
|---|---|---|---|
date | string | yes | Transaction date (YYYY-MM-DD) |
description | string | yes | Human-readable description |
lines | array | yes | Two or more lines (must balance) |
source_type | string | no | Type of originating record, e.g. "payment" |
source_id | string | no | ID of originating record in your system |
reference | string | no | Auto-generated if omitted |
Each line in lines:
| Field | Type | Required | Description |
|---|---|---|---|
account_code | string | either/or | Numeric code e.g. "1020" |
account_label | string | either/or | System label e.g. "PRIMARY_BANK" |
debit | number | yes | Debit amount (0 if credit line) |
credit | number | yes | Credit amount (0 if debit line) |
description | string | no | Line-level note |
account_code or account_label — not both. Total debits must equal total credits.Common Journal Patterns
{
"date": "2026-05-01",
"description": "Rent invoice — Unit 3A — May 2026",
"source_type": "invoice",
"source_id": "inv_001",
"lines": [
{"account_label": "AR_CONTROL", "debit": 750000, "credit": 0},
{"account_label": "RENT_REVENUE", "debit": 0, "credit": 750000}
]
}
{
"date": "2026-05-10",
"description": "Rent payment — Unit 3A — John Doe",
"source_type": "payment",
"source_id": "pay_001",
"lines": [
{"account_label": "PRIMARY_BANK", "debit": 750000, "credit": 0},
{"account_label": "AR_CONTROL", "debit": 0, "credit": 750000}
]
}
{
"date": "2026-05-10",
"description": "M-Pesa rent payment — Unit 3A",
"source_type": "payment",
"source_id": "pay_002",
"lines": [
{"account_label": "MPESA", "debit": 750000, "credit": 0},
{"account_label": "AR_CONTROL", "debit": 0, "credit": 750000}
]
}
{
"date": "2026-05-15",
"description": "Plumbing repair — Block B",
"source_type": "expense",
"source_id": "exp_001",
"lines": [
{"account_label": "MAINTENANCE_EXPENSE", "debit": 120000, "credit": 0},
{"account_label": "PRIMARY_BANK", "debit": 0, "credit": 120000}
]
}
{
"date": "2026-05-01",
"description": "Security deposit — Unit 3A — John Doe",
"source_type": "deposit",
"source_id": "dep_001",
"lines": [
{"account_label": "PRIMARY_BANK", "debit": 1000000, "credit": 0},
{"account_label": "SECURITY_DEPOSIT_LIABILITY", "debit": 0, "credit": 1000000}
]
}
{
"date": "2026-05-31",
"description": "Security deposit refund — Unit 3A — John Doe vacated",
"source_type": "deposit_refund",
"source_id": "ref_001",
"lines": [
{"account_label": "SECURITY_DEPOSIT_LIABILITY", "debit": 1000000, "credit": 0},
{"account_label": "PRIMARY_BANK", "debit": 0, "credit": 1000000}
]
}
Reverse a journal entry
List journal entries
| Param | Type | Description |
|---|---|---|
start_date | string | Filter from date (YYYY-MM-DD) |
end_date | string | Filter to date |
source_type | string | Filter by source type |
source_id | string | Filter by source ID |
limit | integer | Max results (default 100, max 500) |
offset | integer | Pagination offset (default 0) |
Get a single journal entry
Financial Reports
All report endpoints require X-API-Key and return structured JSON.
Trial Balance
All accounts with their net debit/credit balances for the period. Used to verify the books balance.
Income Statement (P&L)
Revenue and expenses for a period, resulting in net income.
Balance Sheet
Assets, liabilities, and equity as of a specific date. Net income is folded into equity automatically.
Cash Position
Sum of all cash and bank account balances. Perfect for a quick dashboard widget.
AR Aging Report
Outstanding receivables bucketed by age. Essential for following up on overdue rent.
| Bucket | Meaning |
|---|---|
current | 0–30 days overdue |
days_31_60 | 31–60 days overdue |
days_61_90 | 61–90 days overdue |
over_90 | More than 90 days overdue |
Bank Reconciliation
Match your accounting records against your bank statement to ensure no transactions are missing or duplicated.
Workflow
Create a reconciliation session
Specify the account, period, and statement closing balance.
List journal lines
View all journal lines for that account in the period.
Toggle lines as cleared
Mark each line that appears on your bank statement.
Lock the reconciliation
Finalize the period to prevent further changes.
Create a reconciliation
| Field | Type | Required | Description |
|---|---|---|---|
account_code | string | yes | Account to reconcile e.g. "1020" |
period_start | string | yes | Start of period |
period_end | string | yes | End of period |
statement_balance | number | yes | Closing balance from bank statement |
List lines / Toggle / Lock
A locked reconciliation returns 409 Conflict if you try to toggle lines.
Error Reference
| Status | Meaning | What to do |
|---|---|---|
| 400 | Validation failed | Check the detail field for the specific reason |
| 401 | Unauthorized | Check your X-API-Key or X-Admin-Key header |
| 404 | Not found | The resource doesn't exist in your tenant |
| 409 | Conflict | Duplicate account code, or locked reconciliation |
| 422 | Unprocessable | Malformed request body (missing field, wrong type) |
| 500 | Server error | Unexpected — check detail and report if persistent |
Common Mistakes
X-API-Key, not Authorization: Bearer. The header name matters./accounts/seed first, or check spelling.Code Examples
import os
import requests
BASE_URL = "https://fedha-api.com"
API_KEY = os.environ["FEDHA_API_KEY"]
def post_journal(date, description, source_type, source_id, lines):
response = requests.post(
f"{BASE_URL}/journal",
headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
json={
"date": str(date),
"description": description,
"source_type": source_type,
"source_id": str(source_id),
"lines": lines,
},
timeout=10,
)
response.raise_for_status()
return response.json()
def void_journal(source_type, source_id):
response = requests.delete(
f"{BASE_URL}/journal/reverse",
headers={"X-API-Key": API_KEY},
params={"source_type": source_type, "source_id": str(source_id)},
timeout=10,
)
response.raise_for_status()
return response.json()
def get_cash_position(as_of_date):
response = requests.get(
f"{BASE_URL}/reports/cash-position",
headers={"X-API-Key": API_KEY},
params={"as_of_date": str(as_of_date)},
timeout=10,
)
response.raise_for_status()
return response.json()
# Usage
post_journal(
date="2026-05-20",
description="Rent payment — Unit 3A",
source_type="payment",
source_id="pay_001",
lines=[
{"account_label": "PRIMARY_BANK", "debit": 750000, "credit": 0},
{"account_label": "AR_CONTROL", "debit": 0, "credit": 750000},
]
)
const BASE_URL = "https://fedha-api.com";
const API_KEY = process.env.FEDHA_API_KEY;
const headers = {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
};
async function postJournal({ date, description, sourceType, sourceId, lines }) {
const res = await fetch(`${BASE_URL}/journal`, {
method: "POST", headers,
body: JSON.stringify({ date, description,
source_type: sourceType, source_id: String(sourceId), lines }),
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
async function voidJournal(sourceType, sourceId) {
const params = new URLSearchParams({ source_type: sourceType, source_id: String(sourceId) });
const res = await fetch(`${BASE_URL}/journal/reverse?${params}`, { method: "DELETE", headers });
if (!res.ok) throw new Error(await res.text());
return res.json();
}
// Usage
await postJournal({
date: "2026-05-20", description: "Rent payment — Unit 3A",
sourceType: "payment", sourceId: "pay_001",
lines: [
{ account_label: "PRIMARY_BANK", debit: 750000, credit: 0 },
{ account_label: "AR_CONTROL", debit: 0, credit: 750000 },
],
});
<?php
define('BASE_URL', 'https://fedha-api.com');
define('API_KEY', getenv('FEDHA_API_KEY'));
function fedha_post(string $path, array $body): array {
$ch = curl_init(BASE_URL . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => ['X-API-Key: ' . API_KEY, 'Content-Type: application/json'],
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
fedha_post('/journal', [
'date' => '2026-05-20', 'description' => 'Rent payment — Unit 3A',
'source_type' => 'payment', 'source_id' => 'pay_001',
'lines' => [
['account_label' => 'PRIMARY_BANK', 'debit' => 750000, 'credit' => 0],
['account_label' => 'AR_CONTROL', 'debit' => 0, 'credit' => 750000],
],
]);