Flow
ServicesBilling

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

OperationRoute
Request a transferPOST /v1/billing-accounts/{account_id}/transfers
Read both directions on an accountGET /v1/billing-accounts/{account_id}/transfers
Read posting status and provenanceGET /v1/billing-account-transfers/{id}
Reverse a transferPOST /v1/billing-account-transfers/{id}/reverse
Correct the transaction datePOST /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.

On this page