docs
/
Courses — AppEngine

AppEngine Accounting and Payroll APIs: Journals, Ledger, Pay Runs and Filing

This recording has not been published on YouTube yet. The written guide is available below.

Written guide

Companion source code

Download the course examples and setup instructions from GitHub. Keep credentials and generated payroll or banking documents out of source control.

You will build: one month of books and payroll for a small café, from a hand-entered rent journal to approved payroll records, a private bank file and an audited period close, entirely through the API.

Why this course

Every business has to keep books and pay its staff, and both are full of rules: every entry must balance, an account code has to exist before money can land in it, pay depends on approved hours, tax tables and deductions, and someone other than the preparer should sign off a pay run. Getting any of it slightly wrong produces numbers nobody can trust.

AppEngine already runs those rules. When you post a journal it checks the lines balance, finds each account, writes the ledger lines and moves the balances. When you process a pay run it saves the pay stubs the approver saw, updates year-to-date totals and books the payroll into the ledger for you. Your app — a manager dashboard, a bookkeeping tool, an integration from another system — calls these actions and shows the results. It does not re-implement them.

Our running example is Harbour Café, one location, in September 2026. We will record the café's rent, process a barista called Maya’s reviewed pay records, watch that payroll appear in the books, and close the month.

1. What AppEngine already does for books and payroll

Built-in data types

A data type (also called a collection) is a kind of record AppEngine already knows, with its fields defined in the SDK. Every saved record comes back in the same envelope: sk is the record's unique ID, datatype names its type, and data holds its business fields. These are the types this course uses:

Data typeWhat it isA few of its fields
ledger_accountOne account in the chart of accountsaccountCode, accountName, accountType (asset, liability, equity, revenue, expense), normalBalance, balance
journal_entryA dated set of debits and credits that must balanceentryDate, description, status (draft, posted…), lineItems[] (ledgerAccountCode, debit, credit), source, referenceType, referenceId
ledger_entryOne posted line of a journal against one accountjournalEntryId, ledgerAccountCode, debit, credit, balanceBefore, balanceAfter
bm_bill, bm_bill_payment, bm_vendorSupplier bills, their payments, and suppliers
bm_employeeA person on the payrollemployeeId (the badge number)
bm_payroll_profileHow that employee is paidemployeeId, payType, hourlyRate, baseSalary, payFrequency, federalTax, stateTax
bm_payroll_runOne pay run for a periodpayPeriodStart, payPeriodEnd, payDate, status, payrollEntries[], totals
bm_pay_stubOne employee's saved pay for one rungrossPay, netPay, taxes, deductions, ytdGrossPay, ytdNetPay

The chart of accounts is the list of ledger_account records. When an organization is set up for Books it gets a standard chart; the codes we use are 1000 Cash, 2200 Payroll Liabilities, 6000 Rent Expense and 6200 Payroll Expense.

Built-in actions

All routes below sit under your API address. Books routes start business-made/books, payroll routes business-made/payroll.

You callAppEngine does for you
POST business-made/books/journalschecks the journal has at least two lines and that debits equal credits, then saves it as a draft
POST business-made/books/journals/:id/postlooks up each account code, writes one ledger line per journal line, moves the account balances, marks the journal posted; if the month is closed it follows your closing-period setting
GET business-made/books/ledger/:accountCodewalks every posted journal for that account and returns the lines with a running balance
GET business-made/books/reports/pl (also balance-sheet, trial-balance, cash-flow)builds the statement from posted entries for the dates and location you ask for
POST business-made/bills/:id/approve and /:id/paybooks the bill (expense against accounts payable), then books the payment from the bank account you name
POST business-made/payroll/run/previewworks out every selected employee's pay from approved timesheets, earning types, deductions and tax tables, without saving anything
POST business-made/payroll/runs/:id/calculatedoes the same calculation and stores it on the run for review, with any missing setup listed
POST business-made/payroll/runs/:id/submit-for-approvalsubmits the calculated run to its configured payroll approval workflow; authorized decision makers act on the returned approval task
POST business-made/payroll/runs/:id/processsaves the exact approved pay stubs, updates year-to-date totals and books the payroll journal; it refuses to pay the same people for the same period twice
POST business-made/payroll/runs/:runId/ach/generatebuilds the direct-deposit bank file for the run's employees
POST business-made/period-close/months/:period/lockcloses a finished month so nothing new lands in it unnoticed

Which to use

  1. Use the built-in action whenever one exists — posting, approving a bill, calculating, approving and processing pay, closing a month. Writing a journal_entry or bm_payroll_run record directly with the generic Repository routes skips everything in the right-hand column: no ledger lines, no balances, no stubs, no booking.
  2. Use the built-in data type for the thing itself. If you need an extra field on a journal or employee, add it to that data type; your organization's own fields are merged over the built-in collection.
  3. Create your own collection only for something AppEngine does not have, such as equipment-maintenance logs, and read and write it with the generic Repository routes taught in Data records.

Before you start

Create or open your organization in Appmint Studio. Use the assigned backend origin shown by your organization and the maintained AppEngine API reference.

You need three values from the platform setup course and authentication course:

  • API — your AppEngine address.
  • ORG — the organization ID, which tells AppEngine whose data you mean. Every call sends it in the orgid header.
  • STAFF_TOKEN — a bearer token: the signed-in staff member's session, sent as Authorization: Bearer ….

Who can call these routes. Books and payroll are back-office areas. Owners and admins can always call them; anyone else needs a role that has been given the /books or /payroll screen in the Role panel (a person with a role named HR can use payroll). An ordinary staff member gets their own payslips from the staff portal instead — see Workforce APIs.

The examples use LOC_HARBOUR as a placeholder for your saved business-location value. The recorded client uses the saved location name apay-harbour-final. Preserve the location value returned by your setup and use it consistently for payroll selection and ledger filters; do not substitute a display label or unrelated record key.

Check the chart. Read the accounts and the account map:

curl -sS "$API/business-made/books/accounts?page=1&pageSize=100" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"
curl -sS "$API/business-made/books/account-map" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"

The account map tells AppEngine which account to use for each kind of automatic posting. Part of the default map:

{ "cash": "1000", "payrollLiabilities": "2200", "payrollExpense": "6200", "payrollTaxExpense": "6200", "salesRevenue": "4000" }

If an account you need is missing, add it with POST business-made/books/accounts and a body such as {"accountCode":"6050","accountName":"Equipment Hire","accountType":"expense"}, or in BusinessMade under Books → Chart of accounts (how-to).

Get Maya ready for payroll. Payroll needs an employer setup, an employee record and that employee's payroll profile. Creating the employee is covered in Workforce APIs; the screens are walked through in BusinessMade Payroll. Then ask AppEngine what is still missing:

curl -sS "$API/business-made/payroll/employer-setup/readiness" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"
curl -sS "$API/business-made/payroll/profiles/employee/E-1001" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"
curl -sS "$API/business-made/payroll/reports/blockers?start=2026-09-14&end=2026-09-20" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"

Maya's employee record has its own sk, and a badge number in data.employeeId — here E-1001. Payroll profiles in this course are looked up by that employee code. The recorded fixture uses APAY-FINAL-MAYA; E-1001 below is a portable placeholder to replace with your own saved employee code. The blockers report lists missing timesheets, timesheets still waiting for approval and employees with no payroll profile; clear those in the workforce screens before you run pay. The earning types, deduction types and tax tables the calculation will use are at GET business-made/payroll/earning-types, deduction-types and tax-rules/tables/status.

Finance client reads accounts and the employee profile
Actual training client. The branch name and employee code determine the requested records; the returned account map supplies automatic posting codes.
journal flow
Concept illustration: the draft has no ledger effect. Posting the same journal produces its debit and credit entries.

3. Record the September rent

Harbour Café paid $1,800 rent from its bank account. That is a debit to Rent Expense and a credit to Cash. A debit and a credit are the two sides of every entry; for a journal to be valid they must add up to the same amount.

Save rent.json:

{
  "date": "2026-09-01",
  "memo": "September rent",
  "businessLocationId": "LOC_HARBOUR",
  "lines": [
    { "account": "6000", "debit": 1800, "credit": 0 },
    { "account": "1000", "debit": 0, "credit": 1800 }
  ]
}

Create the draft:

curl -sS -X POST "$API/business-made/books/journals" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" \
  -H 'Content-Type: application/json' --data-binary @rent.json

AppEngine checks there are at least two lines and that they balance, then saves a journal_entry with status: "draft" and returns it. Keep its sk as JOURNAL_ID. A draft has no effect on any balance yet; it is there to be reviewed. Validate the two totals in your client before submission. The current unbalanced-draft handler returns HTTP500 rather than a useful field error; the rehearsal confirmed no journal was created. Preserve the draft input, show the request failure honestly, and let the user correct it. Do not treat a server failure as a saved journal.

Now post it:

curl -sS -X POST "$API/business-made/books/journals/$JOURNAL_ID/post" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"

Posting is where the accounting happens. AppEngine resolves 6000 and 1000 to the café's own accounts, writes one ledger_entry per line, moves both balances and returns the journal:

{
  "sk": "6710c2…",
  "datatype": "journal_entry",
  "data": {
    "status": "posted",
    "entryDate": "2026-09-01",
    "description": "September rent",
    "businessLocationId": "LOC_HARBOUR",
    "lineItems": [
      { "ledgerAccountCode": "6000", "ledgerAccountName": "Rent Expense", "debit": 1800, "credit": 0 },
      { "ledgerAccountCode": "1000", "ledgerAccountName": "Cash", "debit": 0, "credit": 1800 }
    ],
    "postedBy": "[email protected]",
    "postedAt": "2026-09-08T09:14:02.511Z"
  }
}

If a code does not exist, posting is refused with Ledger account 6050 not found; add the account and post again. If September were already closed, the organization's closing-period setting (GET business-made/period-close/settings) decides what happens to the entry.

Most journals you will never write by hand. Sales, invoices, refunds and supplier bills are booked automatically when they happen through their own actions — approving a bill with POST business-made/bills/:id/approve books the expense against accounts payable, and POST business-made/bills/:id/pay with {"amount":420,"paymentDate":"2026-09-15","method":"ach"} books the payment. Manual journals are for the rest: rent paid outside the system, corrections, accruals.

Balanced journal is saved as a draft
The draft is saved, but the branch ledger still contains no posted rent lines.

4. Check the ledger

The ledger for an account is every posted line against it, in date order, with a running balance:

curl -sS "$API/business-made/books/ledger/6000?fromDate=2026-09-01&toDate=2026-09-30&businessLocationId=LOC_HARBOUR" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"
{
  "accountCode": "6000",
  "total": 1, "page": 1, "pageSize": 50,
  "lines": [
    { "journalId": "6710c2…", "date": "2026-09-01", "memo": "September rent",
      "businessLocationId": "LOC_HARBOUR", "debit": 1800, "credit": 0, "running": 1800 }
  ],
  "totalDebit": 1800, "totalCredit": 0, "endingBalance": 1800
}

To list the month's posted journals, use GET business-made/books/journals?status=posted&fromDate=2026-09-01&toDate=2026-09-30&page=1&pageSize=50.

Posted journal read back as one rent ledger line
Actual post and readback: one 1,800 rent entry, using the same saved journal identity.

5. Run Maya's first payroll

A pay run covers one pay period (here 14–20 September) and a pay date (25 September). It moves through fixed states: draft → review (calculated) → pending_approval → approved → completed (processed), or cancelled. Each move is its own action, so the person who prepares pay and the person who approves it can be different people.

Save pay-run.json:

{
  "payPeriodStart": "2026-09-14",
  "payPeriodEnd": "2026-09-20",
  "payDate": "2026-09-25",
  "employeeIds": ["E-1001"],
  "type": "regular",
  "businessLocationId": "LOC_HARBOUR"
}

Preview first. It runs the full calculation and saves nothing, which is what a "here is what everyone will be paid" screen needs:

curl -sS -X POST "$API/business-made/payroll/run/preview" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" \
  -H 'Content-Type: application/json' --data-binary @pay-run.json

The answer has stubs (one per employee), totals, readiness (ready, missing, warnings) and payReadiness, which lists anyone whose pay is held — calculated and shown, but not to be paid until something such as a missing tax form is resolved, with the reasons.

Create the run with the same body:

curl -sS -X POST "$API/business-made/payroll/runs" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" \
  -H 'Content-Type: application/json' --data-binary @pay-run.json

AppEngine saves a bm_payroll_run as draft, gives it a runNumber and records who prepared it. A run always starts as a draft, whatever status you send. Missing dates return PAYROLL_RUN_DATES_REQUIRED; an end date before the start returns PAYROLL_RUN_BAD_PERIOD. Keep the sk as RUN_ID.

Calculate it:

curl -sS -X POST "$API/business-made/payroll/runs/$RUN_ID/calculate" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"

Maya worked 40 approved hours at $18. The run comes back in review, with her stub stored for the approver:

{
  "sk": "6710c9…",
  "datatype": "bm_payroll_run",
  "data": {
    "runNumber": "RUN-20261009-482913",
    "status": "review",
    "payPeriodStart": "2026-09-14", "payPeriodEnd": "2026-09-20", "payDate": "2026-09-25",
    "preparedBy": "[email protected]",
    "payrollEntries": [
      { "employeeId": "E-1001", "employeeName": "Maya Chen", "payType": "hourly", "regularRate": 18,
        "hours": { "regular": 40, "overtime": 0, "holiday": 0, "pto": 0, "total": 40 },
        "grossPay": 720, "totalTaxes": 99.56, "netPay": 620.44, "employerCosts": 78.84,
        "taxes": [
          { "name": "Federal income tax", "jurisdiction": "federal", "amount": 44.48 },
          { "name": "Social Security", "jurisdiction": "federal", "amount": 44.64, "employerAmount": 44.64 },
          { "name": "Medicare", "jurisdiction": "federal", "amount": 10.44, "employerAmount": 10.44 },
          { "name": "FUTA", "jurisdiction": "federal", "amount": 0, "employerAmount": 4.32 },
          { "name": "TX SUTA", "jurisdiction": "state", "amount": 0, "employerAmount": 19.44 }
        ],
        "warnings": [] }
    ],
    "totals": { "employeeCount": 1, "heldCount": 0, "totalGrossPay": 720, "totalNetPay": 620.44,
                "totalEmployeeTaxes": 99.56, "totalDeductions": 0, "totalCompanyCost": 798.84 },
    "readiness": { "ready": true, "missing": [], "warnings": [] }
  }
}

The exact tax lines depend on Maya's profile and your tax tables. If a timesheet changes while the run is in review, calculate again; a run in draft or review can be recalculated as often as you need. If the calculation itself fails, the run goes back to draft with the reason in calculationError.

Payroll preview shows actual gross taxes and net pay
Configured training inputs return gross 720, taxes 99.56 and net 620.44. Preview does not save a payroll run.
approval flow
Concept illustration: each configured stage requires both decisions. Read the resulting run state after the final task decision.

6. Approve and process

Reference version check. The public reference inspected on 17 October 2026 still lists an older run /approve action and omits /submit-for-approval. The current source and the recorded backend use the task-based flow below. Confirm your deployed version and use this documented contract; do not substitute the obsolete action. The public reference still supplies Books, payroll preview and approval-task contracts.

Submit the reviewed run. The current platform uses a configured approval workflow; there is no direct run /approve action or client selfApprove shortcut.

curl -sS -X POST "$API/business-made/payroll/runs/$RUN_ID/submit-for-approval" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" \
  -H 'Content-Type: application/json' --data '{"comment":"Approved hours and calculation reviewed"}'

Read data.approvalTaskId from the returned run. Keep it separately from RUN_ID. An assigned approver signs in with their own session, reads GET /approval/task/:taskId, checks the saved calculation and records a decision:

curl -sS -X POST "$API/approval/task/$TASK_ID/approve" \
  -H "orgid: $ORG" -H "Authorization: Bearer $APPROVER_TOKEN" \
  -H 'Content-Type: application/json' --data '{"note":"Hours and reviewed amounts checked"}'

Use the task’s rejection action for a rejected review, then read the run again. GET /workflow/position/bm_payroll_run/:runId exposes stage progress. The organization’s workflow determines required stages, approvers and quorum. In this controlled example, Priya and Noah each approve Payroll review, then each approve Payment release. A single decision does not complete a two-approval stage. An approver dropdown in a custom client only chooses which existing session sends the request; it never grants that person a role.

The rehearsal created the standard payroll-approval template only because the training organization had none. For your organization, inspect the existing definition and assignments. Do not replace an established approval policy to make a demo pass. Workflow APIs explains definitions and stages. If no eligible approver can receive the task, correct the workflow/role setup before submission.

Read GET /business-made/payroll/runs/:id after every stage. Enable Process only when the returned status is approved. The backend independently refuses a premature process request; the controlled test returned HTTP400 and created no stubs.

Process. This turns the approved calculation into pay records:

curl -sS -X POST "$API/business-made/payroll/runs/$RUN_ID/process" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"

AppEngine saves one bm_pay_stub per employee using exactly the figures the approver saw, updates each person's year-to-date totals and deduction balances, notifies employees, books the payroll journal and sets the run to completed. It locks the period while it works and checks who has already been paid, so pressing the button twice, or a second run for the same people and dates, does not pay anyone twice.

Processing records pay; it does not send money to anyone's bank. Paying by direct deposit is the bank file in section 8.

Two authorized review decisions advance to Payment release
Priya and Noah each use their own session. Both Payroll review decisions advance the task to Payment release; this intermediate state is not final approval.

7. See the payroll in the books and read September

Read the stubs the run saved:

curl -sS "$API/business-made/payroll/stubs/run/$RUN_ID?page=1&pageSize=50" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"

Each stub has grossPay, netPay, taxes and deductions. Read the payroll profile for the updated year-to-date values: ytdGrossPay, ytdNetPay, ytdTaxes, ytdYear and ytdByYear. Do not assume those profile fields are present on the stub. POST business-made/payroll/stubs/:stubId/pdf/generate makes Maya's payslip PDF and GET business-made/payroll/stubs/:stubId/pdf downloads it.

Now look in the books. Processing posted a journal with source: "payroll", referenceType: "payroll_run" and referenceId set to the run's sk. Its lines come from the account map:

AccountDebitCredit
6200 Payroll Expense — gross wages720.00
6200 Payroll Expense — employer taxes78.84
2200 Payroll Liabilities — withholdings and employer taxes owed178.40
1000 Cash — net pay620.44

GET business-made/books/ledger/6200?fromDate=2026-09-01&toDate=2026-09-30 shows it next to any other payroll this month.

The statements are built from the same posted entries. Always send explicit dates and, if you report per location, the location:

QuestionRoute
What did September earn and cost?GET business-made/books/reports/pl?fromDate=2026-09-01&toDate=2026-09-30&businessLocationId=LOC_HARBOUR
What do we own and owe on 30 September?GET business-made/books/reports/balance-sheet?asOfDate=2026-09-30
Do all debits equal all credits?GET business-made/books/reports/trial-balance?asOfDate=2026-09-30
Where did the cash go?GET business-made/books/reports/cash-flow?fromDate=2026-09-01&toDate=2026-09-30
Who owes us money?GET business-made/books/ar
What do we owe suppliers, and how overdue?GET business-made/bills, GET business-made/bills/aging

The controlled rent journal contains 1,800 and its payroll journal contains 798.84 of expenses. When comparing a P&L, retain the intended organization, period and location filters; other posted entries can change the report total. For payroll-only totals, use GET business-made/payroll/reports/run-totals?runIds=$RUN_ID, reports/ytd?year=2026 and reports/tax-liability?year=2026.

Saved payroll posting and employee year-to-date readback
The actual journal balances wages and employer taxes against liabilities and mapped cash. The profile year-to-date totals match the saved stub.
Actual returned pay stub PDF
Actual PDF generated from the saved stub, rendered without changing its contents. Its pay method is Check at this point; the next section configures direct deposit. This is a controlled training employee.
process outputs
Concept illustration: processing produces linked saved records. A generated artifact is distinct from an external payment.

8. Prepare direct deposit and distinguish generation from payment

Direct deposit in the US goes through an ACH file — a bank file in the NACHA format that your bank collects and pays from. AppEngine builds and tracks the file. File generation does not send it to a bank. This course uses synthetic nonbank details and never transmits the file or calls the online ACH payment action.

Your bank's originator details. Your bank gives you the values that identify you as the sender. Save them once as a setting record named payroll_ach_config using the generic record routes from Data records:

{
  "datatype": "setting",
  "data": {
    "name": "payroll_ach_config",
    "value": {
      "immediateDestination": "…", "immediateOrigin": "…", "companyIdentification": "…",
      "immediateDestinationName": "…", "immediateOriginName": "…", "companyName": "Harbour Cafe",
      "companyEntryDescription": "PAYROLL", "originatingDfi": "…"
    }
  }
}

Maya's bank account. Add it to her profile and choose direct deposit:

curl -sS -X POST "$API/business-made/payroll/profiles/employee/E-1001/bank-accounts" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" -H 'Content-Type: application/json' \
  --data '{"accountType":"checking","bankName":"Harbour Savings","routingNumber":"…","accountNumber":"…","isPrimary":true,"depositType":"remainder"}'
curl -sS -X POST "$API/business-made/payroll/profiles/employee/E-1001/pay-method" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" -H 'Content-Type: application/json' \
  --data '{"paymentMethod":"direct_deposit"}'

The pay method can be direct_deposit, check or cash.

Generate and download the file for the processed run:

curl -sS -X POST "$API/business-made/payroll/runs/$RUN_ID/ach/generate" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" -H 'Content-Type: application/json' --data '{}'
curl -sS -o harbour-payroll.ach "$API/business-made/payroll/runs/$RUN_ID/ach/download" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"

The generate answer has transferCount, totalAmount, effectiveDate and skipped — anyone left out of the file, with the reason (paid by check, no bank account on file, already deposited). Asking again returns the same file; send {"regenerate": true} only when you deliberately want to void the pending transfers and build a new file before it goes to the bank. GET business-made/payroll/ach/transfers?runId=$RUN_ID lists the transfers with account numbers masked. The file contains bank details, so store it as privately as a password.

Saved synthetic primary deposit account and direct-deposit pay method
Actual saved setup uses an explicitly synthetic nonbank account. The private account number and originator identifiers are excluded from the response inspector.

Online payout alternatives. GET business-made/payroll/payout/options reports the configured payout choices and their provider requirements. Plaid originator onboarding has its own POST payout/plaid/onboard action. POST runs/:runId/ach/pay can initiate an online payment; it is not another download button and is excluded from the safe course fixture. Follow the returned account, agreement, onboarding and funding prerequisites for your selected provider. Do not infer bank settlement from a successful payroll process response.

Dates and retries. Inspect the returned effective date before transmission. The past-date rehearsal was moved to the next banking day; do not silently display the old pay date as the deposit date. Repeated file generation reused the same artifact and transfer. Preserve run/transfer identity on retries, and distinguish pending, returned and paid states. The synthetic file must remain private and must never be uploaded to a bank.

Generated bank file summary with adjusted effective date
Actual generated artifact: one transfer totaling 620.44, with the effective date adjusted to 19 October. No bank transmission is performed.

9. Review and close an ended period

At month end the bookkeeper checks the bank, signs off each location and locks the month.

Match the bank. With a bank feed connected, imported bank lines are matched to posted entries. Connecting a bank uses Plaid, configured on the server by the operator (see platform deployment); GET business-made/bank/status tells you if it is set up, and the staff member connecting must have two-step verification on (412 MFA_REQUIRED otherwise). In a sandbox, POST business-made/bank/sandbox/connect with {"institutionId":"ins_109508"} connects Plaid's test bank in one step; then POST business-made/bank/connections/:id/sync imports lines and GET business-made/bank/transactions-page lists them. Match the rent line to our journal with POST business-made/bank/transactions/:id/confirm and {"entryId":"<JOURNAL_ID>"}. A bank line with no entry yet — a bank fee, say — can be booked with POST business-made/bank/transactions/:id/create-entry and {"counterAccountCode":"6500","description":"Bank fee","businessLocationId":"LOC_HARBOUR"}. The finance course covers bank connections for payouts in more depth.

Run the checks and lock. period is the month as YYYY-MM:

curl -sS "$API/business-made/period-close/months/2026-09/checks" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"

This lists the bank reconciliation (with the month's unmatched bank lines) and each location's sign-off. GET business-made/books/reports/close-summary?period=2026-09 gives the month's journal, invoice, bill and payroll figures to review alongside. Record each check only after the matching work is actually done. These are examples of authorized confirmations, not actions the training demonstration has completed:

curl -sS -X POST "$API/business-made/period-close/months/2026-09/bank-reconciliation" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" -H 'Content-Type: application/json' \
  --data '{"done":true,"note":"All September lines matched"}'
curl -sS -X POST "$API/business-made/period-close/months/2026-09/locations/LOC_HARBOUR/sign-off" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" -H 'Content-Type: application/json' \
  --data '{"done":true,"note":"Reviewed P&L"}'
curl -sS -X POST "$API/business-made/period-close/months/2026-09/lock" \
  -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"

Locking is for an owner or admin, and only for a month that has ended. The current month returned PERIOD_NOT_ENDED in rehearsal. The completed September period was locked, then reopened with a reason; the history preserves both actions. The current monthly lock route does not enforce every checklist item. A closed state therefore does not establish that the bank was reconciled or that every location signed off. This demonstration deliberately does not mark an unperformed reconciliation complete. GET business-made/period-close/months?count=3 shows each recent month as open, closed or reopened, with who did it.

Ended period reopened after controlled close
The same ended period is reopened after the controlled close. Reconciliation remains unconfirmed; lock status alone does not establish reconciliation.

10. When something changes or goes wrong

SituationWhat to call
A pay run was set up wrong and has not been processedPOST business-made/payroll/runs/:id/cancel with {"reason":"Wrong period"}. Nothing was paid, so no stubs or books are written; the run is kept with the reason. A processing or completed run returns CANNOT_CANCEL.
An action returns INVALID_STATUSThe run is not in the state that action needs (currentStatus says where it is). Read the run with GET business-made/payroll/runs/:id and take the next step from there.
Your request timed out during posting or processingRead the journal or run first. A journal left in posting is finished with POST business-made/books/resume-postings/:id, which skips lines already written. Never create a second copy.
A sale or payment is missing from the booksGET business-made/books/money-not-in-books returns {count, items}; send one item back to POST business-made/books/money-not-in-books/retry. A 409 carries the reason it still cannot post (often a missing account).
A balance looks wrongGET business-made/books/balance-drift compares cached account balances with the posted entries.
The bank returns a direct depositPOST business-made/payroll/ach/returns with {"transferId":"…","code":"R03","note":"…","date":"2026-09-12"}, or POST ach/returns/import with the bank's return file as {"text":"…"}. If you then pay Maya another way, record it with POST ach/returns/:transferId/reissue and {"method":"check","reference":"1043","date":"2026-09-13"}. The books are corrected for you.
The bank sends a correction to account details (a notice of change)POST business-made/payroll/ach/noc/:transferId/review with {"decision":"apply"} or {"decision":"dismiss"}.
A closed month needs a late entryPOST business-made/period-close/months/:period/reopen with {"reason":"…"}. By default only the owner can reopen, and it is audited.

11. Generate a draft year-end form; keep filing separate

Payroll generates year-end documents from saved stubs. The demonstrated current W-2 route returns a document object with type, taxYear, employeeId, totals, stubCount and pdf; it is not a BaseModel with an sk. Inspect pdf.path/the supported download and compare the totals against the selected employee’s stubs. The controlled one-stub result was 720 wages, 44.48 federal income tax, 44.64 Social Security and 10.44 Medicare. These are observed fixture outputs, not tax advice or a promise about another employee. The older reports/w2 summary returned incorrect zero tax totals for this fixture; use the current generated-form route. No tax filing was transmitted.

The remaining form routes are reference alternatives, not filmed successful submissions:

FormRoute (under business-made/payroll)
W-2 for one employeePOST tax-forms/w2/:employeeId/:taxYear
1099-NEC for one contractorPOST tax-forms/1099-nec/:contractorId/:taxYear
Quarterly 941POST filings/941/:taxYear/:quarter
Annual 940, W-3, EFW2 filePOST filings/940/:taxYear, filings/w3/:taxYear, filings/efw2/:taxYear
List what has been generatedGET tax-forms

Sending forms to the IRS electronically uses the e-file routes under business-made/efile, which only an owner or admin can call (403 EFILE_ADMIN_ONLY otherwise). Save your IRS credentials per program with PUT business-made/efile/connections/:program (tinm for TIN Matching, iris for information returns), check them with POST connections/:program/test, then use POST business-made/efile/tinm/check to match up to 25 payees' tax IDs, or POST business-made/efile/iris/:taxYear/prepare and submissions/:id/transmit for 1099s. IRIS transmission runs against the IRS test environment only in this build; a production attempt returns IRIS_TEST_ONLY.

Draft W-2 returned totals
Actual current W-2 generation returns one included stub and draft-form totals; no filing was transmitted.

12. Finding more and where to go next

Now that you know the shape of Books and Payroll, the API reference finds the rest. Search it by subject:

curl -sS "$API/documentation/search?q=payroll%20run"

Each result links to its full description. There are many more routes than this course used — budgets (business-made/budgets), vendors, statement imports, earning and deduction type management, state quarterly filings. The API reference course shows how to read an entry.

Limitations and checks

Controlled backend rehearsal has verified the original rent journal, payroll workflow decisions, one processed stub and its posting, repeat-process refusal, a private synthetic ACH file and reused generation, current generated W-2 PDF, and ended-period close/reopen. The client procedures are recorded and the 29-chapter, 1440p video has passed independent encoded audiovisual review. No live bank transfer, tax filing, bank reconciliation or employee notification to an external recipient is claimed. Rehearsal used approved entered hours, not 40 hours of actual clock recording.

Run the companion application

Open the companion README for setup. Configure APPENGINE_API_ORIGIN with your assigned backend, then start node browser/server.mjs and open the development address it prints. Sign in with your own authorized staff session. The browser client includes all demonstrated operations; portable JavaScript and Python use operations.json, TypeScript supplies typed composition, and Postman imports the collection. Get the complete companion source on GitHub. The source is published. Watch the reviewed course video.

The preview totals use grossPay, netPay, taxes, employees and held; a calculated run uses totalGrossPay, totalNetPay, totalEmployeeTaxes, employeeCount and heldCount. Normalize those deliberately in your view model rather than silently showing zero for the wrong field.

Further courses