Skip to content

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.

bash
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.

json
{
  "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

bash
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_type is saved as other, an unrecognized direction as borrow, and an unrecognized payment_frequency as monthly (revenue_share for revenue_based).
  • A date that is not YYYY-MM-DD is replaced silently: start_date becomes today and maturity_date becomes the start plus term_months.
  • A current_balance_cents of 0 is replaced by original_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

bash
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, with id, 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_cents and linked_ledger_id.
  • Totals: total_balance_cents (only borrow loans), total_receivable_cents (only lend loans), total_original_principal_cents (only borrow loans), instrument_count, borrow_count, lend_count.
  • cash_accounts: the active bank and cash asset accounts (id, name) you can pass as cash_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:

bash
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

bash
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
json
{
  "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:

bash
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

bash
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).

json
{
  "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

bash
# 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

bash
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

bash
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 lend loan is written off to Bad Debt Expense (credit Notes Receivable, and credit Interest Receivable for accrued interest).
  • A borrow loan 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 stays active.

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:

json
{
  "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

bash
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

bash
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:

json
{
  "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):

bash
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.