Billing account transfers
Move settled held funds between billing accounts, preserve their payment provenance, and correct or reverse a transfer.
A transfer moves settled, unallocated receipt funds between two active billing accounts in the same legal entity and currency. It does not move unpaid invoices, invoice credit, security-deposit charges, cash between entities, or a lease's unit. The accounting app provides a Transfers tab on each billing account. Both accounts show the transfer, its direction, amount, dates, actor, reason, posting state, and original receipt references.
Request and posting
POST /v1/billing-accounts/{account_id}/transfers takes destination_account_id, amount_cents,
reason, and an optional transaction_date under data. An optional post_month must name the
first day of the entity's current Open month. Every command requires Idempotency-Key.
destination_lease_id selects the destination lease when an account has several. Billing infers
its only linked lease when there is one and validates that the lease's property is known. An
account without linked leases remains supported. The accounting app asks for a destination lease
only when a choice is needed. Incoming receipts retain the selected allocation scope.
Controllers and platform admins may request transfers, reverse them, or correct their dates; account readers may view their history.
The default transaction date is the legal entity's local date supplied by Accounting. Typed dates are kept as calendar dates. The post month is held separately. Before posting it shows the Open month at request time; Accounting atomically selects its current Open month when the automated journal actually posts. Billing records that confirmed month, including if delayed work crosses a month boundary. The original receipt can come from an earlier closed month: transferring its held funds creates a new posting and does not change that receipt's original journal.
Approved receipts backed by a posted payment journal and reconciled prepay postings can fund
a transfer. Prepay released by a deposit-funded charge correction can also fund a transfer after
the correction and release have both posted. Its recorded funding source identifies the correction
invoice and initial prepay posting; Billing verifies their accounts and dimensions rather than
treating the release as a new cash payment. transferable_cents on the account reports the available settled amount after existing
reservations. Pending payments, funds already allocated to charges, and funds reserved by another
transfer are excluded. A request that exceeds the available amount fails with 409.
Billing locks both accounts in UUID order, then locks the original payment and source receipt rows in a consistent order. The reservation is committed with its idempotency response, payment provenance, destination receipts, and audit events. Source allocation checks the same reservation, so concurrent transfers and invoice allocations cannot spend the same funds.
The scheduler creates and posts a balanced Accounting journal with a stable idempotency key. It debits the liability account and dimensions that actually hold the source funds and credits the same liability on the destination billing account. Immutable receipt lot payloads retain the funded accounts and source dimensions. The destination liability carries the selected destination lease and its property; it never inherits the source property's tags. Later allocation credits the receivable dimensions from each paid invoice's frozen posting, splitting lines across targets when an unscoped receipt pays several leases. Liability releases and restorations retain their original funded dimensions independently. Moving an application between invoice dimensions reclassifies the receivable even when its amount is unchanged. An allocation with ambiguous historical invoice dimensions remains pending with a visible prepay error instead of guessing.
Later changes to charge-code mappings cannot redirect existing money. Accounting validates the posting month when it posts. No remote call runs inside a Billing transaction. Destination funds remain unavailable until Accounting confirms the posting; Billing then activates the destination receipts and records both prepay balances together. The sum of the accounts' balances is unchanged.
A definitive initial validation or permission refusal marks the transfer Rejected, releases the source reservation, and removes its held destination receipts. Both accounts retain the transfer, receipt provenance, error, and any unposted journal reference for audit. The rejected transfer does not block returning or deleting the original payment. Submit a new transfer after correcting the cause.
Transport failures, uncertain outcomes, temporary errors, and closed-period refusals keep the
reservation and posting_error visible. The scheduler retries committed work after restarts.
It records each remote attempt before sending it and serializes workers without an open database
transaction around Accounting calls. If an earlier response was lost, a later permission refusal
cannot release funds that Accounting may already have moved. A successful idempotent replay
confirms the outcome without creating a second journal or activating funds twice. If posting is delayed across a month boundary, the automated journal posts in Accounting's
current Open month, and both accounts' history shows the confirmed month. The transaction date
stays the date entered. The same rule applies to opposite transfer journals.
Reversal and original-payment withdrawal
POST /v1/billing-account-transfers/{id}/reverse requires a reason. It queues an opposite journal
in the current Open month, then restores the source's funds and reverses destination allocations.
Charges those funds paid reopen. Later source allocation pays only amounts still outstanding;
anything another payment already paid stays paid, and the restored remainder stays held.
If those funds were transferred onward, reversal includes their downstream transfers. All affected transfers must have completed posting and have no pending date correction before reversal begins. Their destination receipts are held against further allocation while the reversal posts. The original transfers, opposite journals, reasons, and payment provenance remain in history.
A return or correction of the original payment unwinds every transfer funded by that payment, including onward transfers, and reopens destination charges. Opposite transfer journals are durable work for the scheduler. A pending transfer or correction must finish before the payment withdrawal can proceed. Transferred receipts cannot be individually returned, deleted, re-registered as payments, or restored by deleting their reversal.
Deleting an original receipt also removes its destination receipts and allocations. The deletion retains receipt and allocation snapshots and includes the transfer and destination prepay journal references for the Payments deletion workflow. Billing checks every affected posting period and rejects pending postings or changes that occur after the period check. Shared transfers restore other original payments to their own account. Transfer records remain visible as deleted audit records. No transferred funds remain available after deletion.
Correcting a transaction date
POST /v1/billing-account-transfers/{id}/transaction-date takes transaction_date and reason.
Billing stores an audited correction request and the scheduler invokes Accounting's source-owned
transaction-date command for that transfer's original journal. Only Billing's service identity can
use that Accounting operation.
Accounting checks the posting month while holding its calendar lock. Controller-requested date
corrections are allowed in Open or Soft-closed posting months and refused in Hard-closed months.
The transaction date is a calendar date, so an earlier or later transaction-date month does not
reclass the posting or move money into or out of that month's balances. The operation changes the transaction date only; the amount,
accounts, post month, original creator, and creation time stay the same. Billing publishes the
corrected date only after Accounting confirms it. Billing rejects requests for a known Hard-closed posting month before reserving a correction.
If Accounting later refuses the request permanently, Billing retains it as Rejected with the
error and releases the pending barrier, so reversal and original-payment withdrawal can proceed.
The user can submit a new correction after reopening the posting month. Transport failures and
unknown outcomes remain pending and retry with the same key; a lost committed response can never
apply the correction twice. The account screen shows the
previous date, corrected date, reason, requesting actor, and completion state.
The Accounting journal list displays occurred_on as the transaction date, so it
shows the confirmed correction without changing the transfer's post month.
API and implementation
| Operation | Route |
|---|---|
| Request a transfer | POST /v1/billing-accounts/{account_id}/transfers |
| Read both directions on an account | GET /v1/billing-accounts/{account_id}/transfers |
| Read posting status and provenance | GET /v1/billing-account-transfers/{id} |
| Reverse a transfer | POST /v1/billing-account-transfers/{id}/reverse |
| Correct the transaction date | POST /v1/billing-account-transfers/{id}/transaction-date |
The account list uses bracket pagination. Lifecycle events carry identifiers, amounts, dates,
state, and receipt provenance. Narrative reasons are emitted separately on the Billing account
PII topic. Database migration 0055 creates the transfer records, receipt provenance, and durable
correction requests. Its down migration refuses to discard existing transfer accounting or audit
records.
Sources: services/billing/internal/handlers/transfers.go, services/billing/internal/transferring/transfer.go, services/billing/internal/transferring/posting.go, and services/billing/migrations/0055_billing_account_transfers.sql.