COUNTCOUNT
Sign Up

Guides

Ledger Semantics & Lifecycles

What each write actually does to the books: which state every operation is valid in, which fields are accepted and then ignored, and which calls cannot be undone. 33 documented behaviours across 12 resources.

The same rules your agent already has

These sections are ported from the topics the COUNT MCP server serves to agents through COUNT_knowledge, so an agent looking up a rule and a developer reading this page get the same answer. Each section shows the COUNT_knowledge topic id it corresponds to.

Accounting APIs fail differently from most REST APIs. A call can return 200 and still not have done what you asked, and a call that succeeds can quietly detach history you expected it to protect. Everything below is behaviour we have observed and confirmed against the live API — including the parts that are surprising. Read the resource you are about to write to before you write to it, and follow Accounting Playbooks for the ordered sequences these rules sit inside.

How to read the severities

Irreversible

Destroys or silently rewrites data, and cannot be undone through the API.

Silent

The call succeeds without doing what you asked — a no-op, or a field accepted then ignored.

Constraint

A constraint that fails loudly, almost always with a 400 you can read and act on.

Irreversible and silent behaviours

Every Irreversible rule on this page, in one place. If you are reviewing what an agent is allowed to do in a production workspace, start here.

ResourceBehaviour
Recurring invoice templatesOmitting nextInvoiceDate on any update nulls it out
Bank transactionsAn empty splits array un-splits the transaction
Manual journal entrieslines replaces every existing line
Vendorscreate_vendor can reactivate and overwrite an inactive vendor
VendorsA successful delete detaches historical rows
Tags and tag groupsdelete_tag detaches the tag from every transaction, without warning
Tasksdelete_task destroys attachments, tags, and recurring schedules
Payroll pay periodsThe batch is not atomic

Grant write access to a test workspace first

None of these behaviours are gated behind a confirmation step at the API layer. If you are wiring an agent to a client’s live books, put your own approval step in front of the writes you care about — see OAuth consent experience for what the user authorizes.

Invoices

Invoices move draft → approved → sent → paid. Approving posts journals, so most write operations behave differently on either side of that step. There is no revert-to-draft route.

draftapprovedsentpartial / paid
OperationValid inConstraint
create_invoiceCreates in draftdueDate is effectively required for invoiceType "invoice" and "estimate" — 400 without it, even though the schema marks it optional. Only credit memos are exempt.
approve_invoicedraftPosts journals. Re-read the invoice afterwards instead of trusting the 200 — see the verification rule below.
send_invoiceapproved400 on a draft. Body fields are to, subject, message, and sendCopy.
update_invoiceAny stateSend only the fields you are changing; internal lifecycle fields are stripped and ignored.
delete_invoiceAny state, including approvedBlocked only by a nonzero paid or refunded amount, or existing payment links — not by being past draft.
assign_transaction_to_bills_invoicesapproved, sent, unpaid, partialNot valid on a draft. Accepts an Income transaction for a normal payment, or an Expense transaction for a refund.
unassign_invoice_transactionpartial, paidUnwinds an applied payment.

Verify that approve_invoice actually appliedSilent

approve_invoice is intended to flip draft → approved, but an empty-body call — the documented standard usage — may not persist approved/isDraft. Always re-read the invoice with get_invoice and check those fields rather than relying on the 200 response alone.

There is no revert-to-draft endpointConstraint

To correct an approved invoice, apply a credit memo. Structural changes are only free before approval, so recreate at draft stage when you still can.

send_invoice ignores recipients, cc, bcc, and attachPdfSilent

Those field names look plausible and are accepted, but do nothing. The real fields are to (defaults to the customer email), subject, message, and sendCopy.

delete_invoice is not draft-onlyConstraint

An approved-but-unpaid invoice deletes cleanly. Do not write logic that expects a draft-only 400 — check the paid and refunded amounts instead.

Bills

Bills move draft → approved → paid. Approval is gated by a real approver workflow, not just by document state, and payment accepts Expense transactions only.

draftapprovedpartial / paid
OperationValid inConstraint
approve_billdraft with line itemsAlso gated by the approver workflow: a caller who is not an assigned approver and not owner/admin gets 403 or 400 depending on approvalStatus, and the bill total can be rejected against the caller’s configured approval dollar limit.
update_bill / delete_billdraftRestricted after approval or payment. delete_bill returns 400 when paidAmount > 0, and also blocks when the bill is linked to a registered fixed asset.
assign_transaction_to_bills_invoicesapprovedExpense transactions only — an Income transaction against a bill always 400s. Pass matchingType "bill".
unassign_bill_transactionpartial, paidwithCaution: true is required to remove a reconciled payment. It is not optional in that case.
apply_vendor_memos_to_billapprovedThe only way to apply a vendor credit memo. Vendor memos never route through assign_transaction_to_bills_invoices.

Approval can fail for permission reasons, not data reasonsConstraint

A well-formed approve_bill call still fails when the caller is not one of the bill’s assigned approvers, or when the total exceeds their approval limit. Treat 403 here as a workflow outcome to surface to the user, not a bug to retry.

Unwind payments before deletingConstraint

delete_bill returns 400 while paidAmount is above zero. Unassign the applied transactions first.

Credit memos

A credit memo is an invoice with invoiceType "memo" and follows the same draft → approved lifecycle. Applying one is how you correct an approved invoice, and every constraint below returns 400 rather than partially applying.

draftapprovedapplied
OperationValid inConstraint
create_invoiceCreates in draftPass invoiceType "memo" and the same customerUuid as the invoice you intend to correct. dueDate is not required for memos.
apply_multiple_credits_to_single_invoiceapproved memo, approved invoiceMany credits onto one invoice.
apply_single_credit_to_multiple_invoicesapproved memo, approved invoicesOne credit spread across several invoices.
remove_invoice_creditappliedThe only way to unwind an application. The apply tools do not work in reverse.

The memo and the invoice must share a customerConstraint

Cross-customer application returns 400 every time. A memo raised against the wrong customer can never be applied to the target invoice — check customerUuid at creation, not at application.

Neither document may be a draft, and a spent memo cannot be reusedConstraint

Both the memo and the invoice must be approved. A memo that is already fully applied or paid cannot be reapplied.

Over-applying rolls back the whole requestConstraint

The applied amount cannot exceed the invoice open balance or the memo remaining balance. If it does, nothing in the request is applied — not even the rows that would have fit.

Recurring invoice templates

New templates are created paused, and the tool that sounds like it un-pauses them does not. This is the single most common reason a recurring schedule silently never fires.

isDraft: true (paused)isDraft: false (active)
OperationValid inConstraint
create_recurring_invoice_templateCreates with isDraft: trueThe template will not generate invoices in this state.
update_recurring_invoice_templateAny stateThe only way to activate a template: send isDraft: false explicitly, together with the current nextInvoiceDate.
resume_recurring_invoice_templateAny stateRequires nextInvoiceDate (400 without it) but only ever sets that field — it never clears isDraft.
list_recurring_invoice_templatesisDraft: false onlyAlways filters to active templates. A paused template is absent from this list by design, not because of a bug.
get_recurring_invoice_templateAny stateNo isDraft filter — use this to check a template’s real state when list does not show it.

Resuming a template does not activate itSilent

resume_recurring_invoice_template and pause_recurring_invoice_template never touch isDraft. A template left at the create-time default stays invisible to list and is skipped by the generation cron, which also requires isDraft: false. Call update_recurring_invoice_template with isDraft: false to actually activate it.

Omitting nextInvoiceDate on any update nulls it outIrreversible

Every update to a template must resend the current nextInvoiceDate, even when that is not the field you meant to change. Leaving it out silently clears the value and pauses the schedule.

Bank transactions

Categorizing, splitting, and deleting a transaction all depend on what it is already linked to. One call in this group destroys data when a field is omitted rather than treating the omission as a no-op.

OperationValid inConstraint
change_transaction_categoryUnlinked transactionsBlocked while the transaction is linked to a bill, an invoice, another linked transaction, or a deposit. Unassign first.
split_transactionUnlinked transactionsLine categories are subject to the same blocked-category list. Never send an empty splits array — see the rule below.
delete_transactionUnlinked, unreviewed, unreconciled, unsplit, non-transferNo override parameter exists on this tool. Soft delete.
create_transactionIncome, Expensetype "Transfer" is not fully supported through the UUID-based tool today — expect a 400 rather than a working transfer.

An empty splits array un-splits the transactionIrreversible

Omitting splits, or sending [], does not no-op. It un-splits an already-split transaction, permanently destroying the split-child rows and restoring the parent to its original amount. Only send an empty array when that is genuinely the intent.

System control accounts can never be a transaction categoryConstraint

Accounts Payable, Accounts Receivable, Retained Earnings, and Accumulated Depreciation are rejected with a 400 on create_transaction, change_transaction_category, and split line categories. Post to those accounts through the documents they belong to instead.

Payment direction is asymmetric between invoices and billsConstraint

assign_transaction_to_bills_invoices accepts an Income or an Expense transaction for an invoice (payment or refund), but only an Expense transaction for a bill. An Income transaction against a bill always 400s.

Manual journal entries

Manual entries are deliberately permissive on create and deliberately strict on edit. Anything COUNT generated itself is read-only.

OperationValid inConstraint
create_journal_entryAlwaysDebits are not required to equal credits. This is intentional — see the rule below.
update_journal_entryManual and Square-integration entriesAlways resend the full lines array. Rejected for system-generated entries and for any entry already linked to a transaction, bill, or invoice.
delete_journal_entryManual and Square-integration entriesA true row destroy, not a soft delete. Rejected for system-generated and linked entries.

Unbalanced entries are allowed on purposeConstraint

create_journal_entry and bulk_create_journal_entries accept postings where debits do not equal credits, to support period-close accruals, payroll clearing, and imports. Do not assume the API will catch an out-of-balance entry for you — validate before posting if your product needs balance enforced.

lines replaces every existing lineIrreversible

When you send lines on an update, the array replaces all existing lines wholesale. Fetch the entry first if you only mean to change one of them. Omitting lines is not a safe alternative — it can trigger a server error rather than leaving the existing lines untouched.

System-generated entries are read-onlyConstraint

Entries produced by invoices, bills, and payroll return 400 on update and delete. Only manually-created and Square-integration entries are editable.

API reference: Journal Entries

COUNT_knowledge → journal_entry_balance_and_lifecycle

Chart of accounts

Some account properties can only be set at creation time, and sub-type ids must be looked up rather than guessed. Finish the chart of accounts before importing anything that posts to it.

OperationValid inConstraint
list_account_sub_typesBefore any createFilter by type — Assets for bank, Liabilities for credit card, Income, or Expenses — and copy the integer id of the matching row.
create_accountAlwaysSupports parentAccountUuid for nesting and taxes (tax UUIDs) at creation time. Both name and subTypeId are required.
update_accountAlwaystaxes is supported and replaces the account’s full set of taxes. Numeric ids are rejected. Re-parenting is not supported — see below.
delete_accountAccounts with no postingsA soft delete: the row and its name stay reserved. Fails with HAS_JOURNAL_ENTRIES once the account has been posted to — set status inactive with update_account instead.

Re-parenting only works at create timeSilent

create_account resolves a parentAccountUuid, but update_account has no UUID-to-id resolution on the update path, so any parent field you send there is silently ignored. Create the account in the right place, or recreate it.

Never guess or probe subTypeIdConstraint

Sub-type ids are sparse global integers, not a predictable sequence. Always read them from list_account_sub_types, or reuse subType.id from an existing account of the same type.

Finish the chart of accounts before importingConstraint

An account that has journal entries can no longer be deleted, only deactivated. Build the full chart first, then import bills and transactions against it.

Delete semantics differ across resourcesConstraint

delete_account and delete_transaction are soft deletes. delete_journal_entry destroys the row for manual entries. Do not generalise one resource’s delete behaviour to another.

Vendors

create_vendor is a find-or-create, not a strict insert, and a successful delete detaches history rather than blocking on it. Both are easy to mistake for something safer than they are.

OperationValid inConstraint
create_vendorAlwaysMatches on normalized name, website, and email. An active match returns 400; an inactive match is reactivated and overwritten — see below.
update_vendor / delete_vendorVendors not linked to a contractorBoth return 400 ("Can not edit/delete a person contractor") when the vendor is linked to a contractor person record. Edit the contractor instead.
delete_vendorNo open bills or memos400 while the vendor has any approved, unpaid bill or vendor memo.

create_vendor can reactivate and overwrite an inactive vendorIrreversible

When the normalized name, website, or email matches an inactive vendor, that vendor is silently reactivated and its fields are overwritten with your payload. You get back an existing, mutated vendor rather than a new one. Call list_vendors first if you need to know whether a name is already in use.

A successful delete detaches historical rowsIrreversible

Once the open-document check passes, deleting a vendor does not block on history: transactions and journal entries that referenced it silently lose their vendor reference. Deactivate instead of deleting when the history matters.

API reference: Vendors

COUNT_knowledge → vendor_find_or_create_and_delete_semantics

Tags and tag groups

Tags accept a field they never store, create is a find-or-create, and delete is the one hard delete in this group with no in-use check.

OperationValid inConstraint
create_tagAlwaysFind-or-create by name: an exact existing name returns the existing tag, still with HTTP 201, rather than erroring or duplicating.
update_tagAlwaysOnly name is persisted.
delete_tagAlwaysA hard delete with no in-use check.
create_tag_group / update_tag_groupAlwaysA tag belongs to one group at a time, and that membership is enforced — 400 on conflict.

Tag color is accepted and never storedSilent

create_tag and update_tag both accept a color field in the schema, but the underlying service only writes name. Do not rely on tag color coming back from the API.

delete_tag detaches the tag from every transaction, without warningIrreversible

There is no in-use check and no confirmation step. The tag is removed from every transaction it was applied to the moment the call succeeds.

A 201 from create_tag does not mean a tag was createdConstraint

Check the returned id against what you sent if your logic depends on having created a new tag rather than found an existing one.

API reference: Tags

COUNT_knowledge → tag_and_tag_group_semantics

Expense receipts

The least-typed surface in the workspace API: create and update forward the body as raw JSON, so every constraint is enforced server-side only. Preflight these calls.

OperationValid inConstraint
create_expense_receipt / update_expense_receiptAlwaysamount is required. expenseReportTypeId and categoryAccountId are conditionally required depending on caller type, unless split rows are supplied. file is required for people-type callers.
match_expense_receipt_manuallyUnmatched Expense transactionsThe target transaction must be type "Expense" — anything else returns 404 "Transaction not found" rather than a clearer validation error. Already-matched transactions return 400.
delete_expense_receiptUnmatched receiptsReturns 409 with error "receipt_matched" — not a generic 400 — while the receipt is matched. Unmatch it first.

No schema validation before the request leavesConstraint

Because the body is forwarded as raw JSON, a typo surfaces as a server-side 400 rather than a client-side schema error. Call validate_payload or describe_endpoint before an unfamiliar create here.

Split receipts must match split transactions line for lineConstraint

When a receipt has split rows, the transaction it matches must also be split, with exactly matching per-line amounts.

API reference: Expense Receipts

COUNT_knowledge → expense_receipt_validation_gaps

Tasks

Task visibility is enforced server-side and differs per tool. Two tools that look interchangeable return different sets of rows.

OperationValid inConstraint
list_tasks / get_taskfirm-team and team-only visibilityVisibility is forced server-side for workspace-scoped sessions. Passing visibility=firm-only does not override it.
list_project_tasksAll rowsApplies no visibility or soft-delete filtering, so firm-only and soft-deleted tasks can appear here even though list_tasks excludes them.
delete_taskAlwaysA soft delete for the task itself, but permanently destructive for what hangs off it.

Firm-only tasks are unreachable from workspace-scoped toolsConstraint

Firm-only rows — notably system-generated INTERNAL_TASK records — cannot be read through list_tasks or get_task no matter what visibility you pass. Firm-scoped sessions use the firm-wide task tools, which apply no visibility filter by default.

delete_task destroys attachments, tags, and recurring schedulesIrreversible

Those are removed permanently as part of the otherwise-soft delete, and do not come back if the task is later undeleted.

Two list tools, two different row setsSilent

If a task appears in list_project_tasks but not list_tasks, that is the visibility model working as designed — not a pagination or caching problem.

API reference: Tasks

COUNT_knowledge → task_visibility_model

Payroll pay periods

update_pay_period applies employee edits one at a time and does not roll back on failure. Read the response arrays rather than treating the call as all-or-nothing.

OperationValid inConstraint
update_pay_periodOpen pay periodsRejects any employee edit that would drive that employee’s calculated net pay negative (payroll_edit_would_make_net_negative), independently of other validation.

The batch is not atomicIrreversible

Employees and their internal sections are applied one at a time in a fixed order. The first failure stops everything after it, but earlier successful updates are not rolled back. Read the response completed, failed, and notAttempted arrays and reconcile from there — never assume the call either fully applied or fully did not.

This tool does not advance payrollConstraint

It never confirms time entries, moves payroll to review, submits, initiates, or skips a run. Those are separate steps outside its scope.

API reference: People

COUNT_knowledge → payroll_update_pay_period_constraints