F FEDHA API ACCOUNTING ENGINE
📘 Developer Documentation

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
⚠️
Security: Never put the 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"}'
🔑
The 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:

rental
Property management, landlords
transport
Freight, logistics, cargo
generic
General business, e-commerce, schools
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

POST /admin/tenants
FieldTypeRequiredDescription
namestringyesDisplay name for the tenant
currencystringnoDefault "TZS". Any 3-letter currency code.

List all tenants

GET /admin/tenants

Get a single tenant

GET /admin/tenants/{tenant_id}

Deactivate a tenant

DELETE /admin/tenants/{tenant_id}

Sets is_active: false. Does not delete data.

Create an API key

POST /admin/tenants/{tenant_id}/keys
FieldTypeRequiredDescription
namestringyesLabel for this key, e.g. "production", "staging"

List API keys for a tenant

GET /admin/tenants/{tenant_id}/keys

Revoke an API key

DELETE /admin/tenants/{tenant_id}/keys/{key_id}

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

POST /accounts/seed?domain={domain}
ParamValuesDescription
domainrental | transport | genericWhich account template to load

Create a custom account

POST /accounts
FieldTypeRequiredDescription
codestringyesUnique numeric code e.g. "6010"
namestringyesHuman-readable name
typestringyesasset | liability | equity | revenue | expense
parent_codestringnoCode of the parent account
system_labelstringnoYour own label for journal lines

List accounts

GET /accounts
ParamTypeDefaultDescription
account_typestringFilter by type
active_onlybooleantrueSet false to include deactivated

Get / Update an account

GET /accounts/{account_code}
PATCH /accounts/{account_code}

Rental Domain — Full Account List

CodeNameTypeLabel
1010Cash on HandassetCASH_ON_HAND
1020Bank AccountassetPRIMARY_BANK
1025Mobile Money — M-PesaassetMPESA
1026Mobile Money — Tigo PesaassetTIGOPESA
1027Mobile Money — HaloPesaassetHALOPESA
1028Mobile Money — Airtel MoneyassetAIRTEL_MONEY
1030Petty CashassetPETTY_CASH
1110Rent ReceivableassetAR_CONTROL
1120Security Deposits HeldassetSECURITY_DEPOSIT_ASSET
1130Input VAT RecoverableassetINPUT_VAT
1310Buildings & Improvementsasset
1320Furniture & Fittingsasset
1390Accumulated DepreciationassetACCUM_DEPRECIATION
2010Accounts PayableliabilityAP_CONTROL
2020Security Deposits PayableliabilitySECURITY_DEPOSIT_LIABILITY
2030VAT PayableliabilityVAT_PAYABLE
2040Accrued ExpensesliabilityACCRUED_EXPENSES
2050Advance Rent ReceivedliabilityADVANCE_RENT
2110Loans PayableliabilityLOANS_PAYABLE
3010Owner's CapitalequityOWNER_CAPITAL
3020Retained EarningsequityRETAINED_EARNINGS
3030Reversal ClearingequityREVERSAL_CLEARING
4010Rent RevenuerevenueRENT_REVENUE
4020Late Fee RevenuerevenueLATE_FEE_REVENUE
4030Service Charge RevenuerevenueSERVICE_CHARGE_REVENUE
4040Other RevenuerevenueOTHER_REVENUE
5010Property MaintenanceexpenseMAINTENANCE_EXPENSE
5020Utilities — Common AreasexpenseUTILITIES_EXPENSE
5030InsuranceexpenseINSURANCE_EXPENSE
5040Property Tax & RatesexpensePROPERTY_TAX_EXPENSE
5050Management FeesexpenseMANAGEMENT_FEE_EXPENSE
5060Salaries & WagesexpenseSALARY_EXPENSE
5070Advertising & MarketingexpenseADVERTISING_EXPENSE
5080Legal & Professional FeesexpenseLEGAL_EXPENSE
5090DepreciationexpenseDEPRECIATION_EXPENSE
5100Office & AdminexpenseADMIN_EXPENSE
5190Miscellaneous ExpensesexpenseMISC_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

POST /journal
FieldTypeRequiredDescription
datestringyesTransaction date (YYYY-MM-DD)
descriptionstringyesHuman-readable description
linesarrayyesTwo or more lines (must balance)
source_typestringnoType of originating record, e.g. "payment"
source_idstringnoID of originating record in your system
referencestringnoAuto-generated if omitted

Each line in lines:

FieldTypeRequiredDescription
account_codestringeither/orNumeric code e.g. "1020"
account_labelstringeither/orSystem label e.g. "PRIMARY_BANK"
debitnumberyesDebit amount (0 if credit line)
creditnumberyesCredit amount (0 if debit line)
descriptionstringnoLine-level note
📌
Each line needs either account_code or account_label — not both. Total debits must equal total credits.

Common Journal Patterns

📄 Rent invoice raised
{
  "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}
  ]
}
💰 Payment received (bank)
{
  "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}
  ]
}
📱 M-Pesa payment received
{
  "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}
  ]
}
🔧 Expense paid (maintenance)
{
  "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}
  ]
}
🔒 Security deposit received
{
  "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}
  ]
}
↩️ Security deposit refunded
{
  "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

DELETE /journal/reverse?source_type={type}&source_id={id}

List journal entries

GET /journal
ParamTypeDescription
start_datestringFilter from date (YYYY-MM-DD)
end_datestringFilter to date
source_typestringFilter by source type
source_idstringFilter by source ID
limitintegerMax results (default 100, max 500)
offsetintegerPagination offset (default 0)

Get a single journal entry

GET /journal/{entry_id}

Financial Reports

All report endpoints require X-API-Key and return structured JSON.

Trial Balance

GET /reports/trial-balance?start_date=...&end_date=...

All accounts with their net debit/credit balances for the period. Used to verify the books balance.

Income Statement (P&L)

GET /reports/income-statement?start_date=...&end_date=...

Revenue and expenses for a period, resulting in net income.

Balance Sheet

GET /reports/balance-sheet?as_of_date=...

Assets, liabilities, and equity as of a specific date. Net income is folded into equity automatically.

Cash Position

GET /reports/cash-position?as_of_date=...

Sum of all cash and bank account balances. Perfect for a quick dashboard widget.

AR Aging Report

GET /reports/ar-aging?as_of_date=...

Outstanding receivables bucketed by age. Essential for following up on overdue rent.

BucketMeaning
current0–30 days overdue
days_31_6031–60 days overdue
days_61_9061–90 days overdue
over_90More 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

POST /reconciliation
FieldTypeRequiredDescription
account_codestringyesAccount to reconcile e.g. "1020"
period_startstringyesStart of period
period_endstringyesEnd of period
statement_balancenumberyesClosing balance from bank statement

List lines / Toggle / Lock

GET /reconciliation/{recon_id}/lines
POST /reconciliation/{recon_id}/toggle-line
POST /reconciliation/{recon_id}/lock

A locked reconciliation returns 409 Conflict if you try to toggle lines.

Error Reference

StatusMeaningWhat to do
400Validation failedCheck the detail field for the specific reason
401UnauthorizedCheck your X-API-Key or X-Admin-Key header
404Not foundThe resource doesn't exist in your tenant
409ConflictDuplicate account code, or locked reconciliation
422UnprocessableMalformed request body (missing field, wrong type)
500Server errorUnexpected — check detail and report if persistent

Common Mistakes

"Provide either account_code or account_label" — Every line needs one of these. You left one blank.
"Journal must balance" — Add up your debits and credits — they must be equal.
401 on every request — Use X-API-Key, not Authorization: Bearer. The header name matters.
404 on account_label — The label doesn't exist. Run /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],
    ],
]);