Online payments
GuestCraft collects online through the workspace’s own Stripe account. Nothing is paid to the project and no account is required: without a connection, folios, cash and manual payments work as before. Why it works this way is in ADR 0054.
Connect a Stripe account
Section titled “Connect a Stripe account”Settings → Payments (payments.configure, built-in: administrators; browser only).
-
Copy the webhook address the page shows and add it in Stripe (Developers → Webhooks), in the mode of the workspace, with the events
checkout.session.completed,checkout.session.expired,payment_intent.succeeded,payment_intent.payment_failed,payment_intent.amount_capturable_updated,payment_intent.canceled,setup_intent.succeeded,setup_intent.setup_failed,refund.created,refund.updated,refund.failed,charge.dispute.created,charge.dispute.closed. Copy its signing secret (whsec_…). -
In Stripe, create a restricted key in the mode of the workspace: a sandbox workspace takes only test keys (
rk_test_…), a production workspace only live keys (rk_live_…). Secret and publishable keys are refused. The key needs these permissions:Stripe resource Access Account Read Customers Write Checkout Sessions Write PaymentIntents Write SetupIntents, PaymentMethods, Charges, Disputes Read Refunds Write -
Paste the key and the signing secret, choose how long payment links last (7 days by default, 1–30), and save. GuestCraft verifies the key by reading the account; the page then shows the account, the mode, the key’s last four characters and when the last signed event arrived.
Verify checks the key again. Disconnect forgets both secrets. Both secrets are sealed with
ENCRYPTION_KEYS and re-encrypted by the rotation command.
Work in progress, and failed events
Section titled “Work in progress, and failed events”The connected page shows the work only this account’s key can finish: open links, payments and
holds Stripe has not settled or released, refunds Stripe has not confirmed, and Stripe events
received and not processed yet (pending on the connection). While any of them is pending,
Disconnect is refused (payments.connection_in_use) and a key of another Stripe account is
refused (payments.account_changed): cancel the links and release the holds, or wait for Stripe
and for the events to be processed.
GuestCraft stores each signed event before it acknowledges it, then processes it in the
background. An event that fails 20 times stops being tried and waits for an operator
(pending.failed_deliveries): the payment or refund it reports is not recorded until it is
processed. Once the cause is fixed, Retry failed events (paymentConnection.retryDeliveries,
at the connection’s version) offers them to processing again with fresh attempts. Failed events
alone do not keep the account connected, so retry them before disconnecting, or they are never
processed.
Payment links
Section titled “Payment links”On a reservation (Payments) or in a folio’s payment dialog, payments.collect (built-in: front
desk, accounting, administrators) creates a link:
| Purpose | What the guest does | What GuestCraft records |
|---|---|---|
| Prepayment | pays the amount, and saves the card for later charges | a card_online payment; the saved card |
| Balance | pays the amount | a card_online payment |
| Fee | pays a posted cancellation or no-show fee | a card_online payment |
| Damage deposit | authorizes the amount (a hold, not a charge) | a hold with its expiry; nothing on the folio |
| Card guarantee | saves a card, pays nothing | the saved card; the card guarantee confirmed |
The amount is at most what the purpose may still collect, after what is already on its way. A folio has one open link per purpose. Copy the link or show its QR code: GuestCraft sends no e-mail or SMS, and never puts a payment link in an OTA conversation. The link is GuestCraft’s address; opening it shows the property, the purpose, the amount and the expiry, and its button takes the guest to Stripe’s own page, where the card is entered. A link expires at its time and can be cancelled. The guest coming back tells nothing: only Stripe’s signed events, or GuestCraft’s reads of Stripe, settle a payment, usually within seconds.
Opening a link makes at most one Stripe page at a time. The button claims the link’s next page in
the database before it asks Stripe, under an idempotency key made of the link and the page’s
number, so asking again for the same page never makes another. While one opening is asking
Stripe, another press of the button shows “Your payment page is being prepared” with the button
again; once the page exists, every press sends the guest to it. A claim older than a minute is
taken over and asked for again with the same key and parameters, which answers the same page. A
page with 30 minutes or less left is replaced only after GuestCraft expired it at Stripe, so a link
never has two pages the guest can pay; while Stripe’s answer to that expiry is unknown the guest
sees that the page is being prepared, and a page the guest completed meanwhile is confirmed as
usual. The guest’s Stripe customer is likewise recorded as requested before it is asked for, with
a random creation key it also carries in its metadata (guestcraft_customer); after a lost answer
GuestCraft looks for customers naming the link and takes only the one carrying that key, made
while the request could make it. If customers name the link without that proof, nothing is
adopted or created: the guest sees that the page is being prepared, and the link shows the
operator “On hold” (customer_unverified, audited as payment_request.customer_unverified).
Cancel it and send a new link after checking who else uses the Stripe account.
Airbnb bookings refuse links, saved-card charges and holds (payments.off_platform, reason
airbnb); another OTA booking may be collected only when its booking facts say the property
collects. An OTA’s virtual card is charged through OTA booking cards.
Payments due
Section titled “Payments due”Payments due (policies.prepayments) lists, by the property’s date, the prepayments due under the
conditions each room was sold with, with the saved card, the open link, a collection in progress
and the last failure. Charge card charges the saved card after you confirm the amount, which
GuestCraft checks again against what is due; Create link sends the guest a link instead. A
declined charge, or one that needs the guest’s authentication, shows the reason; GuestCraft never
retries it.
Holds, fees and refunds
Section titled “Holds, fees and refunds”- Capture a hold: choose the catalog extra and quantity of the damage. GuestCraft posts that charge and captures exactly its amount, at most what the hold still holds; Stripe releases the rest. The payment is posted when Stripe confirms. Release gives the whole hold back. The hold’s expiry, from Stripe, is shown; an expired hold is released.
- Collect a posted cancellation or no-show fee from the saved card, from Policies or the reservation, after confirming the amount.
- Refund to the original card (
payments.refund, built-in: accounting, administrators), from Refunds due or the reservation, up to what the payment received less what is already asked. The refund is booked when Stripe reports it succeeded; a refund that later fails is reversed. A provider payment cannot be refunded or reversed by hand on the folio. - A prepayment link keeps the guest’s card for later charges only when the guest agreed on
Stripe’s page to charges without them and Stripe recorded it (
setup_future_usage: off_sessionon the payment,usage: off_sessionon a card guarantee’s setup). GuestCraft keeps that payment or setup as the card’s consent; a card saved for use only while the guest is present is not kept, and is never charged off-session. - A dispute shows the payment as disputed. A lost dispute reverses the payment, so the balance is owed again; a won one changes nothing.
OTA booking cards
Section titled “OTA booking cards”An OTA booking imported from Channex can carry a card: a virtual card the OTA issued to pay the property (Booking.com, Expedia and others), or the guest’s own card when the property collects. GuestCraft never receives its number: Channex’s Stripe Tokenization App moves the card into the workspace’s own Stripe account as a payment method, and GuestCraft charges that payment method through the same Stripe connection as everything else.
What GuestCraft keeps
Section titled “What GuestCraft keeps”From each accepted booking revision GuestCraft keeps the booking’s payment facts: who collects
(property, ota, or unknown), the payment type, and the card reduced to its brand, last four
digits and expiry, whether it is virtual and, for a virtual card, its currency, its initial balance
as Channex gives it, its effective date and its expiry. The card number, the security code, the
holder’s name and any token Channex shows are dropped while the revision is read; an unknown or
malformed value stays unknown, never a default. A newer revision replaces older facts, never the
reverse. The reservation’s Payments panel shows them (reservationPayments.get, ota_card).
Turn the tokenization app on
Section titled “Turn the tokenization app on”Channex’s committed contract documents one operation of the Stripe Tokenization App, the card
move, and no way to install, list or remove the app. So the administrator installs it in
Channex’s own interface, on the Channex property linked to the GuestCraft property, and Channex
charges for it there. Settings → Payments → Card tokenization through Channex
(payments.configure, browser only) lists the properties linked to Channex with these two steps.
Confirm it is installed asks the administrator to tick that the app is installed in Channex
and that they accept its cost (cardTokenization.acknowledge, installed_and_cost_accepted,
else payments.cost_not_acknowledged); the acknowledgement is audited
(payment_tokenization.acknowledged) and turns the property On. The property must be linked
to a Channex property (payments.channex_not_linked). Channex makes payment methods in the Stripe
account set up for the app at Channex: it must be the account connected here, or GuestCraft
refuses every moved card (foreign).
GuestCraft cannot check the installation in advance: it learns that the app is missing when
Channex answers a card move with 403. The move is then refused (payments.tokenization_missing),
nothing is moved or charged, and Settings and the reservation’s card panel show when Channex
refused (missing_at). After installing the app in Channex, the administrator confirms it again,
or staff simply move the card again: a move Channex answers clears the warning.
Stop (cardTokenization.stop, audited as payment_tokenization.stopped) fences every card
move and charge of the property in GuestCraft at once. It does not uninstall the app in Channex,
which may keep charging for it: the administrator uninstalls it there if they no longer need it.
A stop revokes nothing that Channex already moved: those payment methods stay in the Stripe
account.
Moving cards runs only on the instance that owns the database’s background writes, like Stripe’s money-moving calls; a preview or a restored copy only reads.
Move and charge
Section titled “Move and charge”On the reservation, Move the card to Stripe (otaCards.move, payments.collect) asks Channex
for the payment method of the booking’s card. A booking whose last four digits are unknown or
malformed cannot be matched to any card, so the move is refused before Channex is asked
(payments.ota_card_unavailable, reason unverifiable). GuestCraft keeps the payment method only
when Stripe confirms it is a card of the connected account, in its mode, with the booking’s last
four digits (payments.ota_card_unavailable, reason foreign or mismatch). The move is no new agreement of
the guest’s: it only lets GuestCraft charge what the booking already allows.
Charge the card (otaCardCharges.create, payments.collect) charges the moved card without
the guest, after staff confirm the amount, toward the folio’s balance and at most what is still due
after the collections on their way. The rules:
- A virtual card is the OTA’s settlement of the booking. It is charged from its effective date
to its expiry (on the property’s business date), in its own currency, and within what it can
still pay: Channex’s initial balance less GuestCraft’s own charges of it. A refusal names the
reason:
not_effective,expired,currency,balance, orunknownwhen the booking says too little about the virtual card. The folio books it asota_virtual_card, and the confirmed charge pays the booking’s expected OTA payout in receivables, once (expected OTA payouts). - A guest’s own card is charged only when the booking’s facts say the property collects
(
payments.off_platform,ota_collectsorcollector_unknownotherwise). The folio books it ascard_online. - Airbnb bookings are never moved or charged (
payments.off_platform, reasonairbnb). - One charge of a reservation’s OTA card is on its way at a time (
payments.in_progress); theposting_keymakes a resend answer the same charge. Only Stripe’s signed events, or GuestCraft’s reads of Stripe, book the payment. - The payment method Channex makes belongs to no Stripe customer. A live Stripe account may refuse to charge such a payment method a second time: move the card again, which makes a new one, before the next charge.
PCI DSS
Section titled “PCI DSS”Tokenization reduces what touches card data: GuestCraft stores and transmits no card number or security code, and the card moves from Channex to Stripe directly. It is not a claim that the property has no PCI DSS scope. The deployer and the property remain merchants: their acquirer decides how they validate compliance, and they stay responsible for who may see cards in Channex’s and Stripe’s dashboards, for the accounts and keys of both, for securing the GuestCraft instance and its staff access, and for their own policies. Moving a card is not a new mandate of the guest’s to charge it: the property charges only what the booking and the OTA’s terms allow.
What GuestCraft owns at Stripe
Section titled “What GuestCraft owns at Stripe”A Stripe object changes money in GuestCraft only through a persisted binding: the Stripe identifier GuestCraft recorded when it created or claimed the object (a page, a payment, a refund), in the same workspace and organization, made with the connection’s Stripe account and mode. A dispute belongs to the payment it names. Metadata is a copy anyone with access to the account may write: it only helps find an operation that already claimed the object before Stripe’s answer was lost (a page still being created, a charge or refund still being submitted), and only when everything that operation recorded matches: the amount and currency, the customer, the payment method (a charge), the payment (a refund), the page’s session mode and amount, and whether it is a hold or a charge.
A signed event about an object with no such binding (made in the Stripe dashboard or by another
integration, of another account the connection had, or of another mode) changes nothing: it is
recorded as processed with the outcome unowned and audited as payment_event.unowned. The
webhook address names the organization and the workspace; a delivery to an address whose
organization is not the workspace’s is refused (400) and nothing is stored.
Personal data
Section titled “Personal data”A guest’s erasure (guest data rights) keeps the stay’s payment records under the accounting obligation: provider payments and refunds (a refund’s reason too), the card summary of a payment, a saved card or a moved OTA card (brand, last four digits, expiry), and a saved card’s consent evidence (source, provider object, time). An access export lists them, and the stay’s payment links without their token. Stripe’s identifiers (customers, payment methods, intents, sessions) are identifiers, not personal data. The card facts an OTA booking carries from Channex are cleared with the stay’s personal data; who collects and a virtual card’s terms stay.
Cash settlement and rounding
Section titled “Cash settlement and rounding”In the folio’s payment dialog, Settle in cash pays the folio’s whole balance, or gives a whole credit back, in cash. When the property’s country (Settings → Property) has a country pack with a cash rounding rule for that day, the cash is rounded and the difference is booked as its own Cash rounding line, linked to the cash payment: the cash drawer counts only the cash, and the cash report shows the rounding apart. Without a pack, or outside its coverage, the exact amount is settled. Partial cash payments are never rounded.
API and events
Section titled “API and events”The API changelog lists the operations (paymentConnection, paymentRequests,
reservationPayments, cardCharges, otaCards, otaCardCharges, cardTokenization,
providerPayments, providerRefunds, paymentsDue, cashSettlements). Calls that ask Stripe to move money answer 202 Accepted: the outcome follows.
Webhook events payment_request.succeeded@1, payment_request.failed@1 and
payment_request.expired@1 carry the link’s identifier; folio payments and refunds publish the
existing folio.payment_posted@1 and folio.refund_posted@1.
Testing
Section titled “Testing”Tests never call Stripe or Channex. The Channex simulator models the Stripe Tokenization App’s
one documented operation, POST /bookings/{booking_id}/stripe_payment_method, with its 403 when a
test has not installed the app on the property (setStripeTokenizationApp), and in the server’s
integration test it makes the payment method in the Stripe simulator.
packages/stripe-simulator is an independent simulator of the endpoints
GuestCraft uses, written from Stripe’s published OpenAPI with a citation per behaviour; it signs
and delivers events like Stripe and can lose answers, hold deliveries and replay them out of order.