Skip to main content
A feed is a recurring stream of external money movement (a PSP export, a bank statement, exchange withdrawals) that you reconcile against the ledger. Two resources model it:
  • A source names the external system and carries matching defaults.
  • An external_transaction is one stored row from that system, with a status: open, matched, or ignored.
The ledger never connects to your providers and never holds their credentials. You push rows in, by API call or by pointing a webhook at the hosted inbound endpoint.

Create a source

Names are unique per ledger and lowercased. default_account_id scopes matching to one account for rows that do not carry their own account_id. A name already used by an inline POST /v1/reconciliation_runs exists as an implicit source; creating it explicitly promotes it and applies your defaults.

Ingest rows

Up to 1000 items per request. Ingestion is idempotent on (source, external_id): a re-pushed row never mutates the stored copy and reports replayed. Invalid rows report error per item without blocking the rest. Amounts are integer minor units, sent as a JSON string (an integer is also accepted on write) and always returned as a string. Parse money with a big-integer or decimal type, never a JS number. raw retains the provider payload for audit. reference_rail, reference_kind, and reference_value must be provided together. When the triple resolves to a transaction external ref, matching pairs that transaction’s posting first, exactly, with strategy external_ref. Write the same triple on both sides when you book the internal transaction and reconciliation stops depending on amount heuristics.

Run reconciliation per source

The run draws the source’s open rows in the window. Strategy, window, and tolerance default from the source. Matching passes run in order: external_ref, then exact, then sum_in_window when requested. Runs with at most 500 open rows execute synchronously and return the full result. Larger runs return status: queued; a worker executes them. Poll GET /v1/reconciliation_runs/{id} or subscribe to reconciliation.run_completed. One queued or running run per source; a second request returns run_in_progress.

Work the break queue

status=open after a run is your break list, durable across runs. Three actions resolve a break:
  • POST /v1/external_transactions/:id/ignore with a reason: the row should never match (provider fee booked elsewhere, test row). Emits external_transaction.ignored.
  • POST /v1/external_transactions/:id/match with posting_ids: manual attribution. The postings must be unreconciled, share the row’s currency, and sum exactly to its amount; otherwise 422 with the residual in the hint. Emits reconciliation.match_created.
  • POST /v1/external_transactions/:id/unmatch: undo a match. Postings reopen, the match row is kept with reversed_at for audit, the row returns to open. Emits reconciliation.match_reversed.
GET /v1/external_transactions/:id/matches is the audit trail, including reversed matches.

Hosted inbound endpoint

Instead of polling and pushing from a cron, mint a per-source push URL:
The response contains inbound_url and a one-time inbound_secret. Calling again rotates both; DELETE disables. Sign every push over the exact raw body:
Timestamps more than 5 minutes off are rejected. Unknown or disabled URLs 404. The endpoint ingests with the same idempotency as the batch API and never runs matching; runs stay explicit. A source’s kind selects the payload adapter; custom is this generic scheme, and provider-specific adapters slot in per source without an API change. Re-posting to the enable endpoint is idempotent and keeps the existing URL. Pass {"rotate": true} to mint a new token and secret, or {"provider_secret": "..."} to store a secret issued by the provider instead of a minted one; the response then carries no plaintext secret.

Cobo

Create the source with kind: "cobo" and Cobo’s webhook public key, then paste the inbound_url into the Cobo console as a webhook endpoint. No signing secret is involved; the endpoint verifies Cobo’s Ed25519 signature (BIZ-TIMESTAMP and BIZ-RESP-SIGNATURE headers) against the key.
Only terminal success transaction callbacks ingest (status Success, Succeeded, or Completed); pending, confirming, and failed callbacks are acknowledged and skipped, as are non-transaction events. Deposits are positive, withdrawals negative. Amounts convert from Cobo’s decimal asset units to integer minor units, and each row carries a reference triple (reference_rail is the chain, for example eth, with reference_kind: "tx_hash"), so external-ref matching works out of the box. Redeliveries of the same transaction_id report replayed.

Recipe: reconcile Stripe payouts

  1. Book your internal transaction with an external ref when the charge settles: { "rail": "stripe", "kind": "charge", "value": "ch_3NqXcd" }.
  2. Nightly, list Stripe balance transactions and push them:
  1. Queue a run for the day and alert on reconciliation.run_completed when unmatched_count > 0.
Re-running the script is safe; pages overlap and rows replay. The same mapping works from a Stripe webhook handler that forwards balance.available or charge.succeeded payloads to the inbound URL.

Next steps

Reconciliation

Inline snapshot runs and the matching model.

Webhooks

Event types and signature verification.

Pagination

Cursors on the external transactions list.

API reference

Sources and external transactions endpoints.