Loyalty Cash Credit: Overview
Let members spend their points as cash credit on direct bookings, in-stay folio charges, and at checkout. Every redemption goes through a single Quote → Hold → Confirm/Release lifecycle. The hold reserves points atomically while your integration completes the booking or bill.
Your integration applies the confirmed credit to the booking or bill. Cash credit does not create a reward delivery task or a "Reward to fulfil" email.
If your checkout instead redeems a specific reward (a breakfast, an upgrade) through POST /api/v1/loyalty/redeem, send status: "fulfilled" when your integration has already delivered it. Omitting status uses the reward's configured auto_fulfill setting; otherwise the redemption is pending. Explicit "pending" overrides that setting when a person must still deliver the reward.
Allowed channels are drawn from direct, website, phone, walk_in. Tenants opt in to whichever apply.
The following sources are always blocked, regardless of allowed_channels: booking.com, expedia, agoda, hotelbeds, hotusa, roiback, paraty, mirai, travelclick, siteminder, ota, tour_operator, ttoo.
Rationale: those bookings already carry commission and applying points credit on top is an additional cost for the hotel.
GET /credit/quote
│ (read-only, no DB write)
▼
POST /credit/hold ─────────► loyalty.credit.held
│ (active, expires in hold_minutes)
│
┌───────┼────────┐
│ │ │
▼ ▼ ▼
confirm release expire (cron)
│ │ │
▼ ▼ ▼
loyalty loyalty loyalty
.credit .credit .credit
.confirmed .released .released (cause=expired)
│
▼
loyalty.balance.changed
│
│ (cancellation path)
▼
POST /credit/reverse ─────────► loyalty.credit.reversed
loyalty.balance.changedHolds default to 10 minutes (booking_direct) or 5 minutes (kiosk/front desk). The cronexpire_loyalty_credit_holdssweeps overdue actives on a schedule and emits oneloyalty.credit.releasedwebhook per expired hold (cause = expired).
{ error: { code, message } }.| Code | HTTP | Meaning |
|---|---|---|
| CASH_CREDIT_DISABLED | 403 | Tenant has cash credit turned off. |
| SURFACE_DISABLED | 403 | Surface (booking_direct / in_stay / checkout) is toggled off for this tenant. |
| CHANNEL_NOT_ELIGIBLE | 403 | OTA denylist hit, or channel missing from allowed_channels. |
| INSUFFICIENT_SPENDABLE | 409 | points requested exceeds loyalty_member_spendable(). |
| BELOW_MINIMUM | 422 | points below cash_credit_config.min_points_per_redemption. |
| HOLD_EXPIRED | 410 | Confirm attempted after the hold's expires_at. |
| HOLD_NOT_FOUND | 404 | No hold for the provided hold_id / external_reference_id. |
| MEMBER_NOT_FOUND | 404 | No loyalty member matched the supplied identifier. |
| VALIDATION_ERROR | 400 | Zod schema rejected the payload. Offending field returned. |