POST .../compare, which is also non-destructive (it returns deltas, no writes).
Required scope
The scope table above is the authoritative one for these calls; the API reference lists
migration:read alone on several of them. The dual scope on the GETs means a regular operator with accounting:read can run reconciliation reports during normal operations; a migration-specific key with migration:read (and nothing else) can do the same during a cutover window.
Trial balance
as_of_date=YYYY-MM-DD(default: today). Snapshot date for the trial balance.external_source=versa(default: all). Restrict to journal entries imported from a single source. Use this to verify that ONLY the migrated GL ties; subsequent native postings are excluded.
totals.in_balance equal to true and totals.imbalance equal to 0.00. Anything else is a data error and unfreeze will block.
AR aging
include_pii=false replaces each customer name with [REDACTED], for sharing a report outside your finance team. The aging buckets are measured against the entity’s current business day.
Response:
totals.total must equal the AR control-account balance in the trial balance. That tie is the primary AR proof.
AP aging
Same idea as AR aging, vendor-keyed instead of customer-keyed. Each row carriesvendor_id, display_name, the buckets current, days_1_30, days_31_60, days_61_90 and over_90, and total, with the same bucket names in totals. include_pii=false redacts the vendor names here too, and the object is reconciliation_ap_aging.
Inventory valuation
as_of_date=YYYY-MM-DD(default: today)method(default:fifo) -fifo,avg, orlot_cost; the response names the basis it usedlocation_id(optional) - filter to one warehouse
total_value must equal the Inventory control-account balance in the trial balance.
Cash balance
as_of_date=YYYY-MM-DD (default: today) and bank_account_id to restrict the report to one bank account.
Response:
balance is derived from the posted general ledger entries for that bank account as of the date, and equals the trial-balance value for its GL account. total_cash_balance is the entity-wide cash total. The two unreconciled counts show how many bank lines and GL entries are still waiting to be matched, so a non-zero count is a row to review before you certify the cash balance.
Resource counts
external_source filters to records imported from that source via the bulk endpoint. Omit it to see total counts across all sources, or pass by_source=true to break the journal entry counts down by source type instead.
Compare external records (the penny-match endpoint)
This is the workhorse. Submit external records (from the legacy ERP) and the API returns the per-record verdict against your Arcus data.external_resource and external_records are required; external_source and tolerance_cents are optional. Each record names the thing it is compared to in a different field depending on the resource, and carries the figures to check:
Supported external_resource values:
ar_aging_by_customer- per-customer AR aging buckets.external_idis matched to the customer’s Arcus account id; the figures arecurrent,days_1_30,days_31_60,days_61_90,days_90_plusandtotal.ap_aging_by_vendor- per-vendor AP aging buckets.external_idis matched to the vendor’s Arcus id; the figures arecurrent,days_1_30,days_31_60,days_61_90,over_90andtotal.trial_balance_by_account- per-GL-account totals. The record is matched onaccount_number(orexternal_id); the figures aretotal_debit,total_creditandbalance.resource_counts- object-level counts. The record names the resource inresource_nameand carries its count.
tolerance_cents is the per-record delta you accept (default: 1 cent). Set to 0 for exact ties.
Response:
matched records balance to the penny. mismatched records have at least one field outside tolerance_cents. missing_in_arcus records exist in the external file but not in Arcus. extra_in_arcus records exist in Arcus but not in the external file.
Hard cap: 5000 records per call; more returns 422 external_records_too_large with a hint to split the comparison into batches of 5000 or fewer. An empty or missing external_records returns 400 invalid_field. An external_resource outside the four above returns 422 unsupported_external_resource with the supported list.
Suggested cutover-day workflow
- Pre-cutover (week of): run trial balance + counts daily. Confirm you can reproduce the same totals from the legacy ERP. Spot-check 5 to 10 named accounts via
compare. - Cutover Friday EOD (T-0): freeze the entity. Run final reconciliation suite. Snapshot legacy source data alongside the Arcus database snapshot.
- Cutover Saturday morning: run a top-up bulk-import sync for any data that changed between Thursday’s full load and Friday EOD freeze. Re-run reconciliation.
- Cutover Saturday evening: swap traffic, then run
POST .../cutover/verifywhich fires the full reconciliation suite. This is the gate that unblocks unfreeze. - Cutover Sunday: smoke-test critical user flows. Re-run individual reconciliation reports as needed.
- Cutover Monday go-live: keep the reconciliation key live for a week so the operator can investigate any reported discrepancies.
Error envelope
Like the rest of the public API, every 4xx response follows the canonical envelope:Related
- Cutover orchestration - the freeze / snapshot / swap / verify / unfreeze sequence that consumes these reads.
- Migrating Data into Arcus - the bulk-load step that populates the data these endpoints read.
- Idempotency - request-level idempotency semantics.

