Skip to content

Reconciliation

Providers send you a statement: every payment they collected, every refund they paid out, the fees they charged, and the money they settled to your bank. Reconciliation checks that statement against what zirzir recorded, line by line, and lets you act on what does not agree.

All endpoints use your API key. Each environment (test/live) has its own runs.

Export the statement as CSV and tell zirzir which columns hold what.

Terminal window
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{
"provider": "telebirr",
"currency": "ETB",
"period_start": "2026-10-01",
"period_end": "2026-10-07",
"utc_offset": "+03:00",
"source_name": "telebirr-week-40.csv",
"csv": "Date,Type,Merchant Ref,Transaction ID,Amount\n02/10/2026,C2B,ORD-1,TB-1,\"1,500.00\"\n2026-10-05,Settlement,PAYOUT-1,,\"1,000.00\"\n2026-10-05,Charge,FEE-1,,-12.50\n",
"columns": {"amount": "Amount", "reference": "Merchant Ref",
"provider_txn_id": "Transaction ID", "date": "Date", "kind": "Type"},
"kinds": {"payment": ["C2B"], "payout": ["Settlement"], "fee": ["Charge"]}
}' \
http://localhost:8080/api/v1/reconciliation/runs
FieldMeaning
columnsHeader names. Only amount is required. reference is your payment reference; provider_txn_id is the provider’s own id. Add description to keep a free-text column.
kindsWhich values of the kind column mean payment, refund, payout, payout_reversal (a payout that came back), fee or fee_reversal (a fee credited back). Any other value becomes other.
default_kindThe kind of every line when there is no kind column (default payment).
amount_unitmajor (default, e.g. 1,250.00) or minor (e.g. 125000).
decimal_separator. (default) or , for statements written like 1.250,00 (then set delimiter too).
delimiter, (default), ;, | or tab.
period_start, period_endThe days the statement covers (inclusive).
utc_offsetThe statement’s time zone, e.g. +03:00 for East Africa Time (default +00:00). Used to decide which payments fall in the period.

Amounts may carry thousands separators (1,250.00, 1 250.00), a sign (-75.00, 75.00-, (75.00)) and the currency code (ETB 1,250.00). A comma that is not a thousands separator (12,50) is refused rather than guessed. The sign is kept on the line as negative, but statements disagree on what a sign means, so for payouts and fees the kind decides the direction: map returned payouts and refunded fees to the _reversal kinds.

Dates may be 2026-10-02, 02/10/2026, 02-10-2026, 2026/10/02 or 02.10.2026 (day before month).

Rows with an empty or zero amount (headings, opening balances, subtotals) are not entries: they are counted in the run’s skipped_rows, so check that number. Each line’s line_no is its line in the file (the header is line 1).

An import is all or nothing: if any line cannot be read, nothing is stored and the error names the bad lines. Statements are limited to 5 MB and 20,000 lines; split larger ones by period.

Payment lines are matched to your payments with that provider by reference or provider_txn_id; refund lines to the succeeded refunds of the payment they name. Each line gets a match_status:

StatusMeaning
matchedFound the payment, or a succeeded refund of the same amount
unmatchedNo zirzir record for it, more than one payment fits, or a payment line with a negative amount (a reversal: map its type to refund or other)
duplicateEarlier lines already account for it: the same payment or refund earlier in the statement, or the same payout / fee (same id, amount and day) earlier in the statement or already booked from another statement
amount_mismatchThe payment exists but the amounts differ
currency_mismatchThe payment exists in another currency
status_mismatchThe provider shows it paid but zirzir has it failed or cancelled
not_applicablePayouts, fees, their reversals and other lines: nothing to match, but payouts and fees need booking

A payment line can be matched while zirzir still has the payment as pending: the provider collected the money but zirzir never heard about it (a lost webhook, a customer who closed the page). The line’s match_note shows zirzir’s status.

The response is the run summary: line counts by status, totals per kind in minor units, actionable (lines that still need a decision), and missing_on_statement: completed payments with that provider, paid on a day in the period, that the statement does not list (the first 500; missing_on_statement_count has the total). Investigate those with the provider: the statement may be incomplete, or the payment may not be real.

Terminal window
curl -H "X-API-Key: $KEY" http://localhost:8080/api/v1/reconciliation/runs
curl -H "X-API-Key: $KEY" http://localhost:8080/api/v1/reconciliation/runs/$RUN
curl -H "X-API-Key: $KEY" "http://localhost:8080/api/v1/reconciliation/runs/$RUN/lines?match_status=amount_mismatch"

Lines come in pages of up to 1,000 (limit). When there may be more, the response’s next_after is set: pass it as after to get the next page.

Nothing changes until you resolve a line. Each line is resolved at most once, with one of these actions:

ActionForEffect
settle_paymentA matched payment lineCompletes the payment if zirzir still has it as initiated, pending or processing, and books it in the ledger. Webhooks, splits and wallet top-ups follow as for any completed payment. A payment that is already completed is just marked resolved.
book_payoutA payout or payout_reversal lineA payout moves the amount from your available balance to provider clearing: the money left the provider for your bank. A reversal moves it back.
book_feeA fee or fee_reversal lineA fee moves the amount from your available balance to provider clearing: the provider kept it. A reversal moves it back.
ignoreAny lineRecords that nothing needs doing. Requires a note saying why.
Terminal window
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"action": "ignore", "note": "test payment on the provider sandbox"}' \
http://localhost:8080/api/v1/reconciliation/runs/$RUN/lines/$LINE/resolve

When is a statement trusted to complete a payment?

  • If the provider has a status API (Chapa, ArifPay, SantimPay, LakiPay), zirzir asks the provider instead, and the provider’s answer decides. A statement cannot complete a payment the provider says is still pending.
  • If it has none (Telebirr, and plugins without one), the statement is the evidence, so settle only from statements you downloaded from the provider yourself. A line whose provider id differs from the one zirzir recorded is refused.

Each settlement is written to the audit log with the run and line it came from.

A line is decided once. While a decision runs, the line shows resolving:<resolution>; other decisions on it are refused (retry in a moment), and so is closing the run. If a decision is interrupted (the ledger was unreachable, say), repeat the same decision to finish it: the money still moves only once. A payout or fee with an id (provider_txn_id or reference) can be booked from only one line across all your statements, so importing a statement twice cannot book its payouts twice. Every booking is written to the audit log.

resolve-all applies one action to every unresolved line it fits, and finishes interrupted decisions of the same action:

  • settle_payment: matched payment lines whose payment is not yet completed.
  • book_payout: payout and payout reversal lines that are not duplicates.
  • book_fee: fee and fee reversal lines that are not duplicates.

ignore works one line at a time, because each line needs its own note.

Terminal window
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"action": "settle_payment"}' \
http://localhost:8080/api/v1/reconciliation/runs/$RUN/resolve-all

The response lists each line with ok and, if it failed, the error; one failure does not stop the others.

Terminal window
curl -X POST -H "X-API-Key: $KEY" http://localhost:8080/api/v1/reconciliation/runs/$RUN/close

A closed run is read-only: its lines can no longer be resolved. Close it once actionable is what you expect (ideally 0) and the remaining differences are explained. A run with a decision still resolving cannot be closed until that decision is finished.