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 type | What it is | A few of its fields |
|---|---|---|
ledger_account | One account in the chart of accounts | accountCode, accountName, accountType (asset, liability, equity, revenue, expense), normalBalance, balance |
journal_entry | A dated set of debits and credits that must balance | entryDate, description, status (draft, posted…), lineItems[] (ledgerAccountCode, debit, credit), source, referenceType, referenceId |
ledger_entry | One posted line of a journal against one account | journalEntryId, ledgerAccountCode, debit, credit, balanceBefore, balanceAfter |
bm_bill, bm_bill_payment, bm_vendor | Supplier bills, their payments, and suppliers | |
bm_employee | A person on the payroll | employeeId (the badge number) |
bm_payroll_profile | How that employee is paid | employeeId, payType, hourlyRate, baseSalary, payFrequency, federalTax, stateTax |
bm_payroll_run | One pay run for a period | payPeriodStart, payPeriodEnd, payDate, status, payrollEntries[], totals |
bm_pay_stub | One employee's saved pay for one run | grossPay, 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 call | AppEngine does for you |
|---|---|
POST business-made/books/journals | checks the journal has at least two lines and that debits equal credits, then saves it as a draft |
POST business-made/books/journals/:id/post | looks 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/:accountCode | walks 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/pay | books the bill (expense against accounts payable), then books the payment from the bank account you name |
POST business-made/payroll/run/preview | works out every selected employee's pay from approved timesheets, earning types, deductions and tax tables, without saving anything |
POST business-made/payroll/runs/:id/calculate | does the same calculation and stores it on the run for review, with any missing setup listed |
POST business-made/payroll/runs/:id/submit-for-approval | submits the calculated run to its configured payroll approval workflow; authorized decision makers act on the returned approval task |
POST business-made/payroll/runs/:id/process | saves 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/generate | builds the direct-deposit bank file for the run's employees |
POST business-made/period-close/months/:period/lock | closes a finished month so nothing new lands in it unnoticed |
Which to use
- Use the built-in action whenever one exists — posting, approving a bill, calculating, approving and processing pay, closing a month. Writing a
journal_entryorbm_payroll_runrecord directly with the generic Repository routes skips everything in the right-hand column: no ledger lines, no balances, no stubs, no booking. - 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.
- 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 theorgidheader.STAFF_TOKEN— a bearer token: the signed-in staff member's session, sent asAuthorization: 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.


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

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.

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.jsonThe 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.jsonAppEngine 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.


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.

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:
| Account | Debit | Credit |
|---|---|---|
| 6200 Payroll Expense — gross wages | 720.00 | |
| 6200 Payroll Expense — employer taxes | 78.84 | |
| 2200 Payroll Liabilities — withholdings and employer taxes owed | 178.40 | |
| 1000 Cash — net pay | 620.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:
| Question | Route |
|---|---|
| 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.



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.

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.

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.

10. When something changes or goes wrong
| Situation | What to call |
|---|---|
| A pay run was set up wrong and has not been processed | POST 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_STATUS | The 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 processing | Read 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 books | GET 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 wrong | GET business-made/books/balance-drift compares cached account balances with the posted entries. |
| The bank returns a direct deposit | POST 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 entry | POST 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:
| Form | Route (under business-made/payroll) |
|---|---|
| W-2 for one employee | POST tax-forms/w2/:employeeId/:taxYear |
| 1099-NEC for one contractor | POST tax-forms/1099-nec/:contractorId/:taxYear |
| Quarterly 941 | POST filings/941/:taxYear/:quarter |
| Annual 940, W-3, EFW2 file | POST filings/940/:taxYear, filings/w3/:taxYear, filings/efw2/:taxYear |
| List what has been generated | GET 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.

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
- Workforce APIs — employees, timesheets and the staff portal that feed payroll.
- Finance APIs — taking card payments and refunds, which post into these books.
- Integrations and webhooks — reacting to bank and payment events.