Reservation folios and manual payments
Open a reservation and use Folio. Check-in posts its frozen nightly prices. Add a catalog extra, enter a quantity and service date, and register a manual payment. The check-out dialog previews the balance after releasing unserved nights (at least the first night stays charged). What the dialog asks of you depends on who is leaving:
- Not the last room, with a folio of its own (see Per-room settlement below): the room settles that folio, with the same rules as the last room below for a zero balance, a credit and an amount owed. The rest of the reservation is untouched.
- Not the last room, without a folio of its own: check-out is always allowed. The balance is shown for information and is settled when the last room leaves.
- Last room, balance zero: check out.
- Last room, credit (the guest paid more than the bill, for example a prepaid stay left
early): check-out is allowed with
stays.check_out. The credit stays on the account and appears under Refunds due on the Today screen (folios.credits_due), where a member withfolios.refundopens the account and refunds it. - Last room, balance owed: settle it first. Only a member with
folios.approve_open_balance(built-in: administrators) may confirm check-out with the balance open, with a reason; that approval is audited with the amount, currency, room and reason.
A preview holds no balance: check-out revalidates it inside the writer transaction.
Packages
Section titled “Packages”When a room’s rate plan includes services (its package, see rates),
each served night posts two kinds of charge: Accommodation, the night’s price less its
package, and one line per included service (for example Breakfast × 3) with the service’s own
tax category, source package and quantity. The night’s total is unchanged; the split only
says what it pays for, so tax modules see each service under its category and the revenue report
shows package components apart from room revenue. Package lines follow their room’s nights to
the room’s own folio or to a folio routing room nights.
Each line is reconciled on its own: a change that moves value between the room and its package
reverses and reposts only the lines whose value changed, and a reversed (waived) service stays
waived until its value changes. Posting, as an extra, a service the stay’s package already
includes is refused with folio.included_in_package (409); send beyond_package: true to
charge what the package does not cover, such as a second breakfast. Other extras are charged as
before.
Split folios
Section titled “Split folios”A reservation’s account holds one or more folios, each with its own payer. The first
folio always exists, belongs to the booker and closes only at the last check-out; open up to nine more with
Add folio (folios.split, built-in: front desk, accounting and administrators):
- Payer: the booker, another live guest of the workspace (merging that guest moves the payer to the surviving guest), or a company with its billing details (address, tax number: free text, printed on its statement).
- Room (optional, multi-room reservations): the folio settles that room on its own.
- Routes: which new postings the folio collects. Room nights (not on a room’s own folio,
which already takes that room’s nights), one catalog extra, or every charge of a tax
category (for example
food_beverage). A route belongs to one folio of the reservation: giving it to another folio moves it there.
New postings land on a folio this way: an extra or a payment posted on a named folio stays there; otherwise a folio attached to the charge’s room takes that room’s nights and policy fees, then the folio routing room nights, the extra or its tax category, then the first folio. A payment without a folio goes to the first folio. A tax module’s additional lines follow their charge. Routes skip closed folios. Re-priced nights (a date change, a price correction, a re-rate) stay on the folio of the night they replace.
Move to folio (folios.transfer, built-in: front desk, accounting and administrators)
moves selected charges and payments to another open folio, with a reason. Nothing is
edited or deleted: each moved line gets a pair of transfer movements, one on the folio
it leaves and one on the folio it enters, that net to zero. The reservation’s balance and
its charge, payment and refund totals never change, and a transfer is never revenue. A
charge moves with its additional lines; a payment moves only while it has no refunds; a
reversal, a refund and a transfer do not move. A reversal or a refund is booked on the
folio its movement is on now. Each folio shows its own balance, charges, payments and
refunds: the balance of a folio is the sum of its rows.
Close a folio other than the first once its balance is exactly zero, or, when its company
payer is linked to a counterparty, to move what it owes to that counterparty’s open item
(receivables). The check-out of the
reservation’s last room closes every folio still open, the first one included; undoing that
check-out reopens the folios it closed. Each folio that closes records one folio.closed
event, and each that reopens one folio.reopened; both are webhook and events API types
(folio.closed@1, folio.reopened@1), with the folio as subject. A closed folio takes no new charge and no
transfer until it is reopened (folios.split), which an extra found after departure needs;
payments, refunds and reversals still settle and correct what it holds, so a credit left at
check-out is returned on its folio. Room-charge reconciliation (a channel’s price amendment)
also still posts on a closed folio.
Per-room settlement
Section titled “Per-room settlement”In a multi-room booking, open a folio for one room (Separate folio for room …), move that room’s nights to it, and settle it when the room leaves. Its future nights and policy fees land there by themselves. Check-out of that room, when it is not the last one, looks at the balance of the folios attached to it; the last room still looks at the whole reservation. A credit left on a room’s folio appears under Refunds due as soon as the room has left.
Statements and refunds due
Section titled “Statements and refunds due”Print a statement for one folio, with its payer and billing details, or for the whole reservation. Refunds due lists one line per folio that holds its payer’s money: a folio of a finished or cancelled reservation, or the folio of a room that has left.
Each folio action has its own permission; the built-in roles grant them as follows.
Administrators and accounting manage the property extra catalog (folios.configure_extras).
Front desk, accounting and administrators post extras (folios.post) and manual cash,
external-terminal card, bank transfer or other payments (folios.post_payment). A payment
before check-in is a deposit. Accounting and administrators refund (folios.refund) an
unreversed payment, up to its remaining refundable amount, or reverse a movement
(folios.correct);
both corrections require a reason, and the screen offers each only when it can succeed
(a payment with active refunds cannot be reversed; a fully refunded one has nothing left to
refund). They also recalculate taxes (folios.rerate, below). Front desk settles an owed
balance but does not refund or approve it. Viewer and revenue manager read the folio
(folios.read); housekeeping cannot read folios, catalog prices or checkout balances. A
custom role holds exactly the codes it grants.
The account shows the net view by default: a charge and its reversal disappear as a pair, the nights of one room collapse into a line (“Accommodation · 3 nights”, expandable), and a tax module’s additional lines sit under their charge. Show corrections switches to the full ledger, in posting order, where a reversal reads “Reversal of «Breakfast»”. The printable statement follows the same choice (Include corrections and reversals) and carries the property, the statement date, the guest, the stay, the rooms, the totals and the tax lines.
A same-day check-out still charges the first night, and a late check-in charges the missed
first night. Only a member with folios.correct (built-in: administrators and accounting)
removes it, by reversing the night with a reason. A configurable first-night rule remains planned.
Posting rules: a payment, refund or reversal cannot be dated after the property’s business
date; an extra’s service date is within the stay and cannot be posted to a cancelled
reservation (payments for a cancelled booking, such as deposits, still can). Amounts are
compared as money: retrying 10.5 as 10.500 with the same key is the same operation.
Every posting is immutable, carries its actor and instant, and has a service date and a
posting key. A retry of the same operation and body returns the existing account (or, to a
caller without folios.read, the same acknowledgement as below) without another movement or
audit. Changed intent with the same key returns folio.key_conflict. Clients retain the key
after an uncertain network outcome. Opening a new form represents a new operation. Do not
automatically replay a failed write with a fresh key.
Positive amounts increase debt; payments are negative. Refunds are positive; reversals
negate the original amount and frozen taxes. Payment and refund totals exclude reversed
movements; the balance equals the sum of all signed movements. Gross totals and balances
must fit safe integer minor units. Money on the wire is always { amount: "12.50", currency: "GBP" }, including zero- and three-decimal currencies, without floating-point
conversion in the domain. Folio currency is frozen from the reservation.
In-house departure and price corrections reverse changed nights and post replacements. Unchanged charges keep their original tax snapshot. A manual room-charge reversal is a waiver until that night’s base price changes. Undo check-in/out also reconciles charges; there is no deletion. OTA price amendments use the same reconciler inside intake, including worker processing; failed financial/audit writes roll back the reservation and prevent acknowledgement. Metadata-only revisions preserve charge snapshots. An OTA cancellation reverses only the obligation of a room nobody stayed in: a departed room keeps its stayed nights at their frozen prices and their charges, whatever amounts the cancellation carries, and its reservation stays open for postings (ADR 0028). Price amendments of a room that is not cancelled are reconciled as above. Cancellation posts no room penalties; a deposit needs an explicit refund. Migration 0009 imports frozen charges for already in-house rooms, with system actors, audit and events. Prior off-system payments must be entered explicitly. It does not invent historical balances for reservations that already departed.
HTTP and SDK contracts
Section titled “HTTP and SDK contracts”All paths below are under /api/v1/properties/{property_id}. A session, device token or API
key authenticates, and GuestCraft-Workspace-Id selects the workspace. Folio operations serialize with reservation
writes; they use posting keys rather than reservation ETags. Checkout still requires the
current quoted reservation version in If-Match.
| Operation ID | Method and path | Purpose |
|---|---|---|
folios.get |
GET /reservations/{id}/folio |
The reservation’s account: every folio with its totals, movements, frozen tax lines, signed balance and totals |
folioExtras.list |
GET /extras |
Catalog, including archived entries |
folioExtras.create |
POST /extras |
Name, exact unit price and neutral tax category |
folioExtras.update |
PUT /extras/{extra_id} |
Change or archive a catalog item without changing old postings |
folioTaxCategories.list |
GET /tax-categories |
Category codes the catalog accepts: core codes plus the module’s, with labels |
folioCreditsDue.list |
GET /credits-due |
One line per folio that holds a credit after the stay (refunds due), with its number, name and payer |
folios.postExtra |
POST /reservations/{id}/folio/extras |
posting_key, extra_id, integer quantity, ISO date, optional folio_id (else routed) |
folios.pay |
POST /reservations/{id}/folio/payments |
posting_key, decimal amount, method, reference, date, optional folio_id (else the first folio) |
folios.refund |
POST /reservations/{id}/folio/movements/{movement_id}/refunds |
Payment reference, amount, date, key and reason |
folios.reverse |
POST /reservations/{id}/folio/movements/{movement_id}/reversals |
Original reference, date, key and reason; reverses the charge’s additional lines too |
folios.rerate |
POST /reservations/{id}/folio/rerate |
folios.rerate: calculate taxes again with the installed module; posting_key, reason |
folios.transfer |
POST /reservations/{id}/folio/transfers |
folios.transfer: posting_key, date, target folio_id, movement_ids (1–100), reason |
reservationFolios.list |
GET /reservations/{id}/folios |
The folios, first one first: payer, room, routes, state and version, without money |
reservationFolios.get |
GET /reservations/{id}/folios/{folio_id} |
One folio with its version as ETag |
reservationFolios.create |
POST /reservations/{id}/folios |
folios.split: name, payer, room_id, routes; 201 with ETag and Location |
reservationFolios.update |
PATCH /reservations/{id}/folios/{folio_id} |
folios.split, If-Match: the fields named change |
reservationFolios.close |
POST /reservations/{id}/folios/{folio_id}/close |
folios.split, If-Match: a folio with a zero balance, never the first |
reservationFolios.reopen |
POST /reservations/{id}/folios/{folio_id}/reopen |
folios.split, If-Match: a closed folio |
reservationRooms.checkoutBalance |
GET /reservations/{id}/rooms/{room_id}/checkout_balance |
Read-only quote: signed amount and currency after early checkout adjustments, last_room, scope (room: the amount of the room’s own folios; reservation: the whole account), settlement_required |
reservationRooms.checkOut |
POST /reservations/{id}/rooms/{room_id}/check_out |
Optional { open_balance_reason }; only used, and only with folios.approve_open_balance, when settlement_required is true; ignored otherwise |
Write answers (API revision 4.0.0). folios.postExtra, folios.pay, folios.refund,
folios.reverse, folios.rerate and folios.transfer answer 200 with a FolioWriteResult. A caller that also holds
folios.read for the property receives the Folio. Any other caller receives a
FolioAcknowledgement with only id, workspace_id, property_id and reservation_id: a write
grant alone never reads the account, not even through a retry or a re-rate that changes nothing.
Check for movements before reading money from the answer, or grant the integration folios.read
for the property. This change is an exception to the addition-only rule of /v1, explained in
ADR 0016. The web folio screen requires
folios.read, so a role with only a write grant posts through the API, for example with an API key
limited to folios.post for a point-of-sale integration.
The MIT SDK exposes the operation groups above and generated response validation;
@guestcraft/react/query supplies query factories. Reservation list summaries and Today
stays include optional balance for financial readers (negative is a credit). Each folio
movement also carries reversed_by, reversible, transferable and, for payments,
refundable, so a client never guesses which corrections will be accepted. folio_id is the
folio a movement is booked on and located_in the folio a charge, payment or refund is on
now; a transfer leg names its other folio in counterpart_folio_id. The account’s id is its
first folio’s, as before; folios lists every folio with its own totals. Reservation totals and nightly
prices are also optional: housekeeping projections omit all financial amounts and
open-balance approval reasons. A missing
amount grants no financial access and must not be interpreted as zero.
Tax and statement boundaries
Section titled “Tax and statement boundaries”@guestcraft/core/folios exports the TaxCalculator port (see ADR 0029). The composition
root can inject it through HttpRuntimeOptions.taxCalculator. That adapter also reaches HTTP
intake and authenticated cron workers. A standalone worker accepts it as the third argument to
createBookingWorker, and composeBookingImport accepts taxCalculator explicitly.
interface TaxCalculator { calculate(input: TaxInput): TaxResult | Promise<TaxResult>; // awaited before any write readonly categories?: readonly { code: string; label: string }[];}// TaxInput: propertyId, reservationId, date, category (a code), quantity, base (Money),// stay: { source, roomId, roomTypeId, adults, children, nights, nightIndex }// TaxResult: { computed, lines: TaxLine[] (included or added to the price),// charges?: { rule, label, category, amount }[] (extra neutral charge lines) }Call it inside the posting transaction: read your own tables if you need to, never wait on an
external service. For a per-person, per-night levy, charge stay.adults * rate as an
additional line on accommodation charges; stay.nightIndex and stay.nights give caps.
Categories are canonical codes: the core has accommodation, food_beverage, service
and other; a module adds codes with labels, which the extras catalog then offers. The
core validates currencies, nonnegative safe amounts and total bounds, then freezes the
result. Reversals negate that exact snapshot without calling the adapter. The default
adapter computes no taxes; no country’s tax rules are implemented here.
Charges posted before a module existed keep their uncomputed snapshot until a member with
folios.rerate runs Recalculate taxes on the reservation.
It reverses and reposts only the charges whose result would change, on the folio each charge
is on, never touches base prices or waived charges, and is audited with a reason. A bulk tax
recalculation over a whole property is planned; re-pricing stays at the current rates in bulk
is a different operation, see bulk re-rating.
The UI prints an HTML statement clearly marked non-fiscal. It is not an invoice or
fiscal receipt. Fiscal documents, statutory taxes, company accounts receivable (open items
and allocations across reservations) and OTA collections remain planned.
No external payment service or customer credential is required by this workflow. Online payments
through the workspace’s own Stripe account, and cash settlement with country-pack rounding, are
described in online payments: their folio movements are source: provider (method
card_online) and source: rounding (method cash_rounding), and change only through the
provider or with their cash movement.