Loans and Payroll Reconciliation
This guide covers two route families:
- Loans: a loan subledger on top of debt instruments. Create a loan, record payments, draws and disbursements, accrue interest, write off or forgive a balance, keep a payment schedule, and compare each loan with its ledger account. The app shows this as Loans & Debt in the CFO Portal.
- Payroll:
/payroll/gusto, which matches Gusto's report exports to the Gusto debits on your bank account, splits each payroll lump per employee, and books taxes and reimbursements through bills. Preview first, then apply.
All requests send Authorization: Bearer dz_your_token_here and x-business-id: YOUR_BUSINESS_ID. Money is integer cents, interest rates are integer basis points (500 = 5.00%), and dates are YYYY-MM-DD. Responses use the {"success": true, "data": ...} envelope and the examples show data. Errors follow Error Handling.
Access and Permissions
| Family | Base path | Add-on | Read | Write |
|---|---|---|---|---|
| Loan subledger | /api/v1/loans |
CFO Suite | GET |
POST, PATCH |
| Loan set-up, payments, schedule | /api/v1/businesses/{business_id}/cfo-portal/debt-schedule |
CFO Suite | GET |
POST, PATCH; DELETE needs delete permission |
| Gusto payroll | /api/v1/payroll/gusto |
none | GET lines, POST reconcile/preview |
POST reconcile/apply |
Both loan families need the CFO Suite add-on. It is unlocked during a trial, for sandbox workspaces, and for businesses on a firm Professional or Enterprise plan; otherwise every call is 403 with code BILLING_003. The payroll routes have no add-on gate. In the CFO Portal paths, {business_id} must equal the x-business-id header, otherwise the call is 403. A read_only token cannot call POST, PATCH or DELETE routes (see Authentication).
Loan POST calls return 201. The Gusto POSTs, every PATCH and every DELETE return 200.
Loan Concepts
Every loan is a debt instrument with a direction:
direction |
Meaning | Principal | Interest |
|---|---|---|---|
borrow |
A loan to you (the default). The counterparty is the lender | Liability (Long-Term Debt) | Expense |
lend |
A loan you made (a note receivable). The counterparty is the borrower | Asset (Notes Receivable) | Income |
| Field | Values |
|---|---|
status |
active, paid_off, written_off |
debt_type |
term_loan, line_of_credit, mortgage, equipment_loan, sba_loan, convertible_note, revenue_based, other |
payment_frequency |
monthly, quarterly, semi_annual, annual, bullet, revenue_share |
counterparty_type |
customer, vendor, shareholder, employee, affiliate, other |
register entry_type |
disbursement, payment, interest_accrual, write_off, adjustment |
Every action below posts a balanced journal entry (source: system) and appends a row to the loan's register, an append-only list showing the running principal and accrued interest after each event, with the journal_entry_id. The system ledgers (Notes Receivable, Long-Term Debt, Interest Income, Interest Expense, Interest Receivable, Accrued Interest Payable, Bad Debt Expense, Debt Forgiveness Income) are created in your chart of accounts on first use.
Create a Loan
Parse a contract (optional)
Upload the PDF first (see File Uploads); the s3_key must belong to your business.
curl -X POST "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/debt-schedule/parse-contract" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"s3_key": "YOUR_BUSINESS_ID/0192abcd-term-sheet.pdf"}'The AI reads the contract and returns draft terms and a projected payout table. Nothing is saved.
{
"contract": {
"name": "Equipment loan",
"lender": "First Bank",
"direction": "borrow",
"debt_type": "equipment_loan",
"original_principal_cents": 5000000,
"current_balance_cents": 5000000,
"interest_rate_bps": 650,
"term_months": 36,
"start_date": "2026-01-15",
"maturity_date": "2029-01-15",
"payment_frequency": "monthly",
"payment_amount_cents": 153000,
"revenue_share_bps": null,
"total_repayment_cap_cents": null,
"summary": "36-month equipment loan at 6.5%.",
"confidence": 0.92
},
"payment_schedule": [
{
"payment_date": "2026-02-15",
"amount_cents": 153000,
"principal_cents": 126000,
"interest_cents": 27000,
"remaining_balance_cents": 4874000
}
]
}payment_schedule is a preview of at most 12 rows. For revenue_based financing it is a single "until cap" row, since the real payments depend on future revenue. The parser decides direction by matching your business name to the parties in the contract and defaults to borrow; check it before approving. A password-protected PDF, an unreadable file, or one with almost no text (a scan without a text layer) is 400; an s3_key that belongs to another business is 403.
Approve the terms
curl -X POST "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/debt-schedule/approve-contract" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"contract": {
"name": "Loan to Acme Supply",
"lender": "Acme Supply",
"direction": "lend",
"debt_type": "term_loan",
"original_principal_cents": 5000000,
"current_balance_cents": 5000000,
"interest_rate_bps": 800,
"term_months": 24,
"start_date": "2026-03-01",
"maturity_date": "2028-03-01",
"payment_frequency": "monthly",
"payment_amount_cents": 226000
},
"contract_s3_key": null
}'You do not have to call parse-contract first: approve-contract accepts any contract object, so it is also the way to create a loan by hand. The response is {"id", "name", "lender"}.
contract field |
Notes |
|---|---|
name, lender |
Required, 1 to 255 characters. lender is the counterparty, whichever side they are on |
debt_type, original_principal_cents, current_balance_cents, start_date, maturity_date |
Required |
direction |
Default borrow |
interest_rate_bps, term_months, payment_amount_cents |
Default 0 |
payment_frequency |
Default monthly |
revenue_share_bps, total_repayment_cap_cents |
For revenue_based debt |
summary |
Up to 2,000 characters; stored as the loan's notes |
confidence |
0 to 1; informational |
contract_s3_key stores the original document on the loan. Behavior to know:
- An unrecognized
debt_typeis saved asother, an unrecognizeddirectionasborrow, and an unrecognizedpayment_frequencyasmonthly(revenue_shareforrevenue_based). - A date that is not
YYYY-MM-DDis replaced silently:start_datebecomes today andmaturity_datebecomes the start plusterm_months. - A
current_balance_centsof0is replaced byoriginal_principal_cents. - Approving also generates the loan's paydown table (below); the loan is created even if that step fails.
- New loans start with
counterparty_type: other; change it with the re-tag call.
List Loans and GL Reconciliation
curl "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/debt-schedule" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"The response is a CFO Portal section: business_id, section, generated_at, sources and a data object with:
instruments: every loan that is not removed, withid,name,lender,direction,debt_type,original_principal_cents,current_balance_cents,interest_rate_pct(a percentage, not basis points),term_months,start_date,maturity_date,payment_amount_cents,payment_frequency,next_payment_date,months_remaining,status,notes,covenant_count,covenants,is_revenue_based,revenue_share_bps,total_repayment_cap_cents,amount_repaid_centsandlinked_ledger_id.- Totals:
total_balance_cents(onlyborrowloans),total_receivable_cents(onlylendloans),total_original_principal_cents(onlyborrowloans),instrument_count,borrow_count,lend_count. cash_accounts: the active bank and cash asset accounts (id,name) you can pass ascash_ledger_id.payment_schedule: each open loan's next payment (instrument_id,name,direction,next_payment_date,next_payment_cents,maturity_date,is_overdue), soonest first. Loans with no balance, no upcoming date, or revenue-share payments are left out.
If the section fails to load, the call still returns 200 with an empty data and a sources entry whose status is unavailable. Check for that before trusting an empty list.
Reconcile a loan to the general ledger
Link a loan to the chart-of-accounts ledger that carries its balance:
curl -X PATCH "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/debt-schedule/instruments/LOAN_ID" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"linked_ledger_id": "LONG_TERM_DEBT_LEDGER_UUID"}'Send "" to clear the link; a ledger id from another business is 404. Once linked, each instrument in the list also carries linked_ledger_name, linked_ledger_balance_cents (the ledger's balance as of today), balance_drift_cents (loan balance minus ledger balance), reconciliation_status and has_balance_drift.
reconciliation_status |
Meaning |
|---|---|
reconciled |
Within 1 cent of the ledger |
drift |
More than 1 cent apart; has_balance_drift is true |
not_on_gl |
The loan has a balance but the ledger balance is zero |
A loan with no link (or whose ledger was deleted) has no status.
The same PATCH updates name, current_balance_cents, notes, amount_repaid_cents, revenue_share_bps and total_repayment_cap_cents; an empty body is 400. These edit the loan record only and post no journal entry, so use payments, draws and write-offs to move balances through the books.
Record a Payment
curl -X POST "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/debt-schedule/instruments/LOAN_ID/payments" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"payment_amount_cents": 153000,
"cash_ledger_id": "CHECKING_LEDGER_UUID",
"payment_date": "2026-02-15",
"memo": "February installment"
}'payment_amount_cents (greater than 0) and cash_ledger_id are required unless you send transaction_id (next section). payment_date defaults to today and memo (500 characters) replaces the generated journal description.
DayZero splits the payment with the amortization rule: interest is the period's share of the rate on the current balance, and the rest is principal (up to the balance). For a zero-rate or revenue-based loan the payment goes to principal up to the balance, and any excess is booked as interest. If interest was already accrued (below), the interest portion first relieves that accrual and only the remainder is recognized as new expense or income. The entry is:
| Direction | Cash | Principal | Accrued interest | New interest |
|---|---|---|---|---|
borrow |
credit | debit Long-Term Debt | debit Accrued Interest Payable | debit Interest Expense |
lend |
debit | credit Notes Receivable | credit Interest Receivable | credit Interest Income |
{
"journal_entry_id": "019ab37c-c057-7000-8000-0000000000a1",
"principal_cents": 126000,
"interest_cents": 27000,
"remaining_balance_cents": 4874000,
"name": "Equipment loan",
"direction": "borrow",
"transaction_id": null
}The balance drops by the principal portion. When both the balance and the accrued interest reach zero an active loan becomes paid_off.
Link a bank transaction to a loan payment
Pass the id of a bank transaction that already has a journal entry and DayZero rebooks that entry into the principal and interest split instead of creating a second one:
curl -X POST "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/debt-schedule/instruments/LOAN_ID/payments" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"transaction_id": "TRANSACTION_UUID"}'With transaction_id the amount is the transaction's absolute amount, the payment date defaults to the transaction's date, and cash_ledger_id defaults to the transaction's bank account. The register row keeps the transaction_id, and the transaction's categorization is marked as set by a user.
| Status | Cause |
|---|---|
409 |
The transaction is already linked to a loan payment (CONFLICT_031) |
404 |
Unknown transaction (NOT_FOUND_012) or loan (NOT_FOUND_001) |
400 |
The transaction has no journal entry or no identifiable bank line, or already has invoice or bill payment allocations |
Draws, Disbursements, Interest and Write-offs
These are the /loans routes. {instrument_id} is the loan's id. Request bodies on /loans reject unknown fields. A loan that is written_off rejects accruals and disbursements with 409 (CONFLICT_090).
Subledger summary
curl "https://api.ondayzero.com/api/v1/loans/subledger?direction=lend" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Loans grouped by counterparty (the loan's lender name, matched case-insensitively), largest total first. direction is optional; when sent it must be lend or borrow, and anything else returns 400 (GEN_002).
{
"counterparty_count": 1,
"loan_count": 1,
"total_lent_outstanding_cents": 4200000,
"total_borrowed_outstanding_cents": 0,
"total_accrued_receivable_cents": 28000,
"total_accrued_payable_cents": 0,
"groups": [
{
"counterparty": "Acme Supply",
"counterparty_type": "customer",
"loan_count": 1,
"outstanding_principal_cents": 4200000,
"accrued_interest_cents": 28000,
"total_receivable_cents": 4228000,
"loans": [
{
"id": "019ab37c-c057-7000-8000-0000000000b2",
"name": "Loan to Acme Supply",
"counterparty": "Acme Supply",
"counterparty_type": "customer",
"direction": "lend",
"debt_type": "term_loan",
"status": "active",
"interest_rate_bps": 800,
"original_principal_cents": 5000000,
"current_balance_cents": 4200000,
"accrued_interest_cents": 28000,
"written_off_cents": 0,
"start_date": "2026-03-01",
"maturity_date": "2028-03-01",
"last_accrual_date": "2026-06-30"
}
]
}
]
}total_receivable_cents is principal plus accrued interest for the group.
A loan's register
GET /api/v1/loans/{instrument_id}/subledger returns instrument (the same loan summary as above), entries (oldest first) and entry_count. Each entry has id, debt_instrument_id, entry_type, entry_date, amount_cents, principal_delta_cents, interest_delta_cents, balance_after_cents, accrued_interest_after_cents, journal_entry_id and memo.
Accrue interest
# One loan
curl -X POST "https://api.ondayzero.com/api/v1/loans/LOAN_ID/accrue-interest" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"through_date": "2026-06-30", "memo": "June accrual"}'
# Every active loan (safe as a month-end batch)
curl -X POST "https://api.ondayzero.com/api/v1/loans/accrue-interest" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"through_date": "2026-06-30"}'Interest is simple actual/365 on the current balance, from the last accrual date (or the loan's start date) through through_date: balance x rate x days / 365, rounded to the cent. The entry debits Interest Receivable and credits Interest Income for a lend loan, and debits Interest Expense and credits Accrued Interest Payable for a borrow loan. The accrued amount is added to the loan and relieved by the next payment.
The single-loan call (memo up to 500 characters) returns instrument_id, instrument_name, period_start, through_date, days, interest_cents, accrued_interest_cents and journal_entry_id. It is 400 (VALIDATION_349) when there is nothing to accrue: a zero rate, a zero balance, a revenue-based loan, or a through_date on or before the last accrual. The batch call skips those loans instead of failing and returns through_date, instruments_accrued, instruments_skipped (which includes loans that are not active), total_interest_cents and results (one entry per accrued loan, shaped like the single-loan response).
Disburse or draw
curl -X POST "https://api.ondayzero.com/api/v1/loans/LOAN_ID/disbursements" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"amount_cents": 1000000, "cash_ledger_id": "CHECKING_LEDGER_UUID", "disbursement_date": "2026-04-01"}'Records more money moving on an existing loan. amount_cents (greater than 0) and cash_ledger_id are required; disbursement_date defaults to today and memo (500 characters) is optional. On a lend loan it debits Notes Receivable and credits cash (you advanced more); on a borrow loan it debits cash and credits Long-Term Debt (you drew more, for example on a line of credit). The loan balance increases and a paid_off loan returns to active. The response has instrument_id, instrument_name, amount_cents, new_balance_cents and journal_entry_id. A cash_ledger_id that is not in your business is 400 (NOT_FOUND_014).
Write off or forgive
curl -X POST "https://api.ondayzero.com/api/v1/loans/LOAN_ID/write-off" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"principal_cents": 500000, "write_off_date": "2026-06-30", "memo": "Partial forgiveness"}'All fields are optional (principal_cents must be greater than 0 when sent). Omit principal_cents to write off the full remaining balance. write_off_date defaults to today.
- A
lendloan is written off to Bad Debt Expense (credit Notes Receivable, and credit Interest Receivable for accrued interest). - A
borrowloan is forgiven: Long-Term Debt (and Accrued Interest Payable) are debited and Debt Forgiveness Income is credited. - A full write-off also clears the accrued interest and sets the status to
written_off. A partial write-off reduces principal only and the loan staysactive.
The response has instrument_id, instrument_name, principal_written_off_cents, interest_written_off_cents, remaining_balance_cents, status and journal_entry_id. A principal_cents above the balance is 400 (VALIDATION_350); a loan already written off is 409. There is no API call to reverse a write-off; reverse its journal_entry_id with Journal Entries if you need to undo it.
Re-tag the counterparty
PATCH /api/v1/loans/{instrument_id}/counterparty with {"counterparty_type": "shareholder"} changes the classification used for grouping and shareholder-loan reporting. It returns instrument_id and counterparty_type.
Paydown Schedule
Each loan has an editable table of expected payments. It feeds the cash flow forecast: only rows still scheduled, partially_paid or overdue count as future cash. All schedule calls return the whole table:
{
"instrument_id": "019ab37c-c057-7000-8000-0000000000b2",
"name": "Loan to Acme Supply",
"direction": "lend",
"current_balance_cents": 4200000,
"rows": [
{
"id": "019ab37c-c057-7000-8000-0000000000c3",
"debt_instrument_id": "019ab37c-c057-7000-8000-0000000000b2",
"sequence": 1,
"due_date": "2026-07-01",
"principal_cents": 172000,
"interest_cents": 28000,
"total_cents": 200000,
"expected_balance_after_cents": 4028000,
"status": "scheduled",
"is_custom_edited": false,
"paid_amount_cents": 0,
"note": null
}
],
"total_principal_cents": 4200000,
"total_interest_cents": 310000,
"total_payment_cents": 4510000
}All paths below start with /api/v1/businesses/{business_id}/cfo-portal/debt-schedule.
| Method and path | Body | Notes |
|---|---|---|
GET /instruments/{instrument_id}/payment-schedule |
none | The saved table |
POST /instruments/{instrument_id}/payment-schedule/generate |
overwrite_custom (default false) |
Rebuilds from the loan's terms. With false, rows you edited by hand are kept and generated rows on the same dates are skipped; with true the whole table is replaced |
POST /instruments/{instrument_id}/payment-schedule/rows |
due_date (required), principal_cents, interest_cents (default 0), note (500 characters) |
Adds a custom row |
PATCH /payment-schedule/rows/{row_id} |
any of due_date, principal_cents, interest_cents, status, note |
status is scheduled, paid, partially_paid, skipped or overdue; an unknown value or empty body is 400 |
DELETE /payment-schedule/rows/{row_id} |
none | Removes the row |
Rows are renumbered by date and expected_balance_after_cents is recomputed after every change. Editing a date or an amount marks the row is_custom_edited. A scheduled row whose date has passed is reported as overdue without being saved that way. Generation amortizes the current balance from the next payment date to maturity (a fixed payment_amount_cents is used when set, up to 600 periods); bullet, revenue_share and revenue_based loans produce one row, and a loan with no balance produces none. Recording a payment does not mark schedule rows paid; update the row's status yourself if you track it there.
Covenants and Removal
| Call | Notes |
|---|---|
GET /instruments/{instrument_id}/covenants |
Lists covenants: id, metric_name, operator, threshold_value, measurement_frequency, description |
POST /instruments/{instrument_id}/covenants |
Adds one. Required: metric_name (up to 100 characters), operator (greater_than, less_than, greater_than_or_equal, less_than_or_equal), threshold_value (a string up to 50 characters, such as "1.25"). Optional: measurement_frequency (a payment frequency value, default quarterly), description. An unknown operator or measurement_frequency is 400 |
DELETE /covenants/{covenant_id} |
Removes one covenant |
DELETE /instruments/{instrument_id} |
Removes the loan from the schedule (a soft delete). Journal entries it already posted are kept |
Gusto Payroll Reconciliation
Gusto debits your bank account with a few kinds of lines and no detail: one lump for every employee's net pay, one lump for payroll taxes, separate lines per contractor payment, reimbursements, and fees. This family matches those bank rows to Gusto's own report exports so each one is described, assigned to a counterparty and ledger, and split per employee. It does not connect to Gusto: you supply the exports.
Export these from Gusto > Reports as CSV or Excel, unedited: the Payroll Journal (or Payroll Register), Contractor Payments, and Bank Transactions reports. DayZero detects each type from its columns and skips title and total rows.
Bank rows are the non-deleted transactions in the date range whose description mentions Gusto (or its old name, ZenPayroll), classified from the descriptor:
kind |
What the bank line is | Example descriptor |
|---|---|---|
net |
Net pay for one payroll, a lump for all employees | GUSTO NET 260235 |
tax |
Payroll taxes for one payroll | GUSTO TAX 260235 |
contractor_us, contractor_intl |
One contractor payment (domestic, international) | GUSTO CND 524974 - Lance LeVan - US contractor payment, GUSTO ICD ... |
reimbursement |
Employee reimbursements | GUSTO REM - ... |
fee |
A Gusto fee | GUSTO FEE - Payroll Fee |
wire |
A payroll funded by wire | WIRE TRANSFER TO GUSTO |
unknown |
A Gusto debit that could not be classified |
List Gusto bank lines
curl "https://api.ondayzero.com/api/v1/payroll/gusto/lines?start_date=2026-01-01&end_date=2026-03-31" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"start_date and end_date are required, and the range may span at most 400 days (otherwise 400, VALIDATION_505). bank_ledger_id narrows the list to one account; the default is every account. The response has items, total and totals_by_kind (cents per kind). Each item has transaction_id, date, amount (absolute cents), description, counterparty, kind, gusto_id, payee, ledger_name (the row's current category), reconciled (a Gusto reconciliation has already applied it) and pay_date.
Preview
curl -X POST "https://api.ondayzero.com/api/v1/payroll/gusto/reconcile/preview" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"start_date": "2026-01-01",
"end_date": "2026-03-31",
"bank_ledger_id": "OPERATING_CHECKING_UUID",
"reports": [
{ "s3_key": "YOUR_BUSINESS_ID/0192abcd-payroll-journal.csv", "filename": "payroll-journal.csv" },
{ "s3_key": "YOUR_BUSINESS_ID/0192abce-contractor-payments.csv", "filename": "contractor-payments.csv" }
]
}'Preview is a POST but only needs read permission and writes nothing, not even the payroll ledgers.
| Field | Description |
|---|---|
start_date, end_date |
Required. Bank dates considered; at most 400 days apart |
bank_ledger_id |
The account Gusto debits. Default: every bank account. Must be one of the business's bank or credit-card accounts |
reports |
Up to 10 Gusto exports. Each is exactly one of s3_key (an uploaded CSV or XLSX) or content (pasted CSV or tab-separated text, up to 2,000,000 characters), plus an optional filename |
ledger_overrides |
Book to your own ledger ids instead of the system ones: wages, taxes, contractors, reimbursements, fees |
amount_tolerance_cents |
0 to 500 (default 0). The largest gap accepted between a bank lump and the register total; the difference becomes a rounding leg |
create_bills |
Default true. Books tax and reimbursement debits as an accrual bill per pay period (vendor Gusto), paid by the bank row. false splits or categorizes them directly |
Reports are optional: with none, only what the bank descriptors say (a contractor's name, a fee) can be booked. A file that is not a Gusto report is 400 (VALIDATION_502) and an unreadable one is 400 (VALIDATION_501); an s3_key that belongs to another business is 403.
Matching: a net or tax lump is paired with a payroll in the register by amount, within roughly 7 days before to 3 days after the check date (10 before to 1 after for a wire); contractor lines pair by amount within 2 days of the contractor report; the bank transactions report settles lines whose descriptor says nothing.
The response:
| Field | Description |
|---|---|
reports |
A summary per file: filename, kind (payroll_register, contractor_payments or bank_transactions), rows_read, rows_skipped, payroll_runs, contractor_payments, debits |
ledgers |
The ledger each category will use (key, ledger_id, ledger_name, will_create) |
lines |
One plan per bank row (below) |
outstanding |
Items with no partner on the other side: report lines with no bank row, and bank rows with no report line. Each has source (payroll_register, contractor_report, bank_report or ledger for a bank row), kind, date, amount, name, transaction_id and reason |
counts |
Rows per status, plus outstanding |
plan_hash |
A digest of the plan, to pass to apply |
Each lines item has transaction_id, date, amount, kind, status, match_source (payroll_register, contractor_report, bank_report or description), pay_date, period_start, period_end, date_offset_days, amount_difference, the proposed new_description, new_counterparty, ledger_key, ledger_id and ledger_name, notes, and either a split (net lumps: one leg per employee with employee, amount, ledger_key and counterparty_name) or a bill (taxes and reimbursements: action of create or link_existing, vendor_name, bill_date, due_date, amount, lines). One net lump looks like this:
{
"transaction_id": "019ab37c-c057-7000-8000-0000000000d1",
"date": "2026-02-10",
"amount": 1284500,
"kind": "net",
"status": "matched",
"match_source": "payroll_register",
"pay_date": "2026-02-13",
"period_start": "2026-01-26",
"period_end": "2026-02-08",
"date_offset_days": -3,
"amount_difference": 0,
"new_description": "GUSTO NET 260235 · Payroll 2026-02-13 · 3 employees",
"new_counterparty": "Gusto",
"ledger_key": "wages",
"ledger_name": "Payroll Expense",
"split": [
{ "employee": "Ana Ruiz", "amount": 420000, "ledger_key": "wages", "counterparty_name": "Ana Ruiz" },
{ "employee": "Ben Cho", "amount": 380500, "ledger_key": "wages", "counterparty_name": "Ben Cho" },
{ "employee": "Cy Diaz", "amount": 484000, "ledger_key": "wages", "counterparty_name": "Cy Diaz" }
]
}status |
Meaning |
|---|---|
matched |
Paired with a report line; will be applied |
categorize_only |
Not in a report, but the descriptor identifies it (a named contractor, a fee, a reimbursement); will be applied |
unmatched |
Nothing to pair it with; left alone and listed in outstanding |
already_applied |
A previous reconciliation applied it; skipped |
Apply
Send the same body as the preview, plus the preview's plan_hash and, optionally, transaction_ids to apply only those bank rows (up to 2,000):
curl -X POST "https://api.ondayzero.com/api/v1/payroll/gusto/reconcile/apply" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"start_date": "2026-01-01",
"end_date": "2026-03-31",
"bank_ledger_id": "OPERATING_CHECKING_UUID",
"reports": [
{ "s3_key": "YOUR_BUSINESS_ID/0192abcd-payroll-journal.csv", "filename": "payroll-journal.csv" },
{ "s3_key": "YOUR_BUSINESS_ID/0192abce-contractor-payments.csv", "filename": "contractor-payments.csv" }
],
"plan_hash": "PLAN_HASH_FROM_PREVIEW"
}'Apply recomputes the plan from the inputs and, if plan_hash is sent and the plan has changed (rows were edited or bills changed since the preview), refuses with 400 (VALIDATION_506); run the preview again. It then, for each matched or categorize_only row, renames it, sets its counterparty, and either splits the net lump per employee, creates the pay period's Gusto bill and pays it with the bank row (or pays an open Gusto bill of the same amount dated within 45 days before to 7 days after the debit), or categorizes it. The payroll ledgers (Payroll Expense, Payroll Taxes, Contract Labor, Employee Reimbursements, Payroll Processing Fees) are created on first use unless you override them, and a vendor named Gusto is created if none exists. It also saves the register as pay runs for the payroll report and adds any employee it has not seen to your people roster.
Each row is applied on its own, so one failure does not undo the others. The response is the refreshed preview plus counters: transactions_updated, splits_written, bills_created, bills_paid, employees_created, pay_runs_saved and failures (each transaction_id with an error). Rows are stamped as applied, so repeating the call skips them and is safe. At most 5,000 Gusto rows may be in range (400, VALIDATION_507); narrow the dates if you hit it.
Errors
| Status | Code | When |
|---|---|---|
400 |
GEN_002 |
A required field is missing or malformed (including a payment with neither transaction_id nor payment_amount_cents and cash_ledger_id). The errors map names the fields |
400 |
VALIDATION_001 |
Empty PATCH body, unknown schedule status, covenant operator or measurement_frequency, or an unreadable or empty contract PDF |
400 |
VALIDATION_349 |
Nothing to accrue on that loan for that period |
400 |
VALIDATION_350 |
Write-off larger than the loan's balance |
400 |
VALIDATION_005 |
A write-off or linked-transaction payment resolves to a zero amount (for example, nothing left to write off) |
400 |
NOT_FOUND_014 |
cash_ledger_id or a Gusto ledger_overrides id is not a ledger of this business |
400 |
VALIDATION_501, VALIDATION_502 |
A Gusto report could not be read, or is not a Gusto report |
400 |
VALIDATION_504, VALIDATION_505 |
Invalid Gusto bank_ledger_id, or a bad date range |
400 |
VALIDATION_506, VALIDATION_507 |
Stale Gusto plan, or too many Gusto rows |
403 |
BILLING_003 |
The CFO Suite add-on is missing (/loans and the CFO Portal loan routes) |
403 |
AUTH_002 |
{business_id} in a CFO Portal path differs from x-business-id, or an s3_key belongs to another business |
404 |
NOT_FOUND_098 |
Loan not found (also for another business's loan); a missing schedule row uses the same code. The payment route returns NOT_FOUND_001 for an unknown loan, and a missing covenant returns the same code |
409 |
CONFLICT_090 |
The loan is already written off |
409 |
CONFLICT_031 |
The bank transaction is already linked to a loan payment |
Send an Idempotency-Key header on the POST and PATCH calls you may retry; the payment, accrual, disbursement and write-off calls each post a new journal entry. For the bank transactions behind these entries see Transactions.
Not Covered
The CFO Portal has many other sections under /businesses/{business_id}/cfo-portal (forecasts, budgets versus actuals, statements); only the debt-schedule routes are described here. Partner credit lines and their /credit/loans routes are a separate product, documented in Credit. Regent can read the loan subledger and accrue interest in chat. The MCP server has no loan tools; its apply_gusto_reconciliation tool runs the payroll reconciliation (dry_run: true previews), see MCP.