Deposits and no-shows
The booking policy protects a slot with a deposit (paid up front, taken off the bill), a card on file (saved uncharged, for a later late-cancellation or no-show fee), or both from one card entry. Payments count towards any fee: a £10.00$10.00€10.00 deposit and a £15.00$15.00€15.00 late fee leave £5.00$5.00€5.00 to charge, never £25.00$25.00€25.00.
Take a deposit
Section titled “Take a deposit”To take a deposit when a client books, set the policy’s deposit to fixed, percent (rounded
down) or full, and choose whether it’s refundable on a timely cancellation.
| Booked… | What happens |
|---|---|
| On the booking page or widget | Shown before the client commits (Continue to pay the £10.00$10.00€10.00 deposit). The booking exists only once it’s paid. If the card is declined, the client tries another or the slot is released. |
| Any other way (desk, API, agent) | Confirmed at once with a deposit_amount due, not collected. Use POST /v1/bookings/{id}/payments with kind: "deposit"; the client pays at its approval_url. |
The deposit comes off amount_due:
| Outcome | The deposit |
|---|---|
| Client cancels in time | Refunded, if refundable |
| Client cancels late, or doesn’t come | Kept, towards the fee |
| You cancel | Refunded in full, even a non-refundable one |
Keep a card on file
Section titled “Keep a card on file”Set card_required (always, new_clients or over_amount), card_after_no_shows (1–10
no-shows), or both.
- Without a deposit, a card step saves the card uncharged, and saving it makes the booking.
- The card’s mandate uses the policy’s words and names its possible fees. The largest caps any one charge.
- Clients with a covering card (saved under a booking policy, valid in the visit’s month) aren’t asked again.
When a client doesn’t come, or cancels late
Section titled “When a client doesn’t come, or cancels late”| How | Notes | |
|---|---|---|
| No-show | No-show on the calendar, or POST /v1/bookings/{id}/no_show, after the start | auto_no_show marks a confirmed, unchecked-in booking 2 hours after its end. Undo with …/complete within a day, unless the fee is charged. |
| Late cancel | POST /v1/bookings/{id}/cancel with by: "client" inside the free-cancellation window, or the client’s own link | Keeps the deposit; the late fee becomes due. GET /v1/bookings/{id}/cancellation?by=client previews refund_amount, deposit_kept, fee_due. |
The booking’s fee shows what’s due:
{ "kind": "no_show", "amount": 2500, "deposit_applied": 0, "status": "due", "clause": "If you don't turn up, a fee of 100% (£25.00) applies." }{ "kind": "no_show", "amount": 2500, "deposit_applied": 0, "status": "due", "clause": "If you don't turn up, a fee of 100% ($25.00) applies." }{ "kind": "no_show", "amount": 2500, "deposit_applied": 0, "status": "due", "clause": "If you don't turn up, a fee of 100% (€25.00) applies." }kind is no_show or late_cancel; clause is the policy sentence behind it.
Charge the fee
Section titled “Charge the fee”Dashboard: Fees groups fees by state: Due, Waiting for the client, Refused, Charged, Waived, Refunded. Charge confirms the amount, clause and mode; Waive records a reason. The app requires Face ID to charge.
API: POST /v1/bookings/{id}/charge_fee charges the card on file once, within 14 days of the
appointment (then 410 fee_window_closed). Send an Idempotency-Key, and amount to charge less.
It returns the payment; check its status:
| Result | The fee | Event |
|---|---|---|
| The bank took it | charged; booking payment_status → fee_charged | booking.fee_charged |
| The bank wants the client to confirm | pending_authentication; the client gets an email link to confirm | booking.fee_charged once they do |
| The bank refused | failed, reason in fee.failure | booking.fee_failed |
| Other calls | |
|---|---|
409 no_card_on_file | No covering card. Send a pay link: POST /v1/bookings/{id}/payments with kind: "no_show_fee" or "late_cancel_fee". |
POST /v1/bookings/{id}/waive_fee | Waives it, with a reason. You can still charge it within the 14 days. |
POST /v1/refunds | Refunds a charged fee like any payment. |
Automatically: auto_charge charges a no-show fee 2 hours after marking and a late-cancel fee
at once, the same way and once. Refusals, bank authentication and missing cards are left to
you. A booking you moved after the client’s window closed is never charged.
Disputes: evidence is drafted from the booking: the accepted policy (words, version, when and where), the card mandate, who marked the no-show, the receipt and every message. You review and submit it.
Try it in test mode
Section titled “Try it in test mode”Mark a no-show, try to charge without a card, then waive the fee.
POST /v1/test_helpers/bookings/{id}/force_no_show skips the wait. The SDKs’ request covers
endpoints without a method.
api() { # GET a path, or POST it with a body, using your key and a fresh Idempotency-Key if [ $# -eq 1 ]; then curl -sS "https://api.tuppence.ai$1" -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" else curl -sS "https://api.tuppence.ai$1" -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" -d "$2" fi}# Not turning up costs the whole price.api /v1/booking_policy '{"no_show_fee": {"type": "percent", "value": 100}}' > /dev/null
# A service, someone who does it, and tomorrow's first free time with them.location=$(api /v1/locations | jq -r '.data[0].id')service=$(api /v1/services '{"name": "Coaching session", "duration_minutes": 30, "amount": 2500}' | jq -r .id)kai=$(api /v1/staff "{\"name\": \"Kai\", \"locations\": [\"$location\"]}" | jq -r .id)d='[["09:00", "17:00"]]'api "/v1/staff/$kai/schedules" "{\"location\": \"$location\", \"effective_from\": \"$(date +%F)\", \"pattern\": [{\"1\": $d, \"2\": $d, \"3\": $d, \"4\": $d, \"5\": $d, \"6\": $d, \"7\": $d}]}" > /dev/nulltomorrow=$(date -v+1d +%F 2>/dev/null || date -d "+1 day" +%F)starts_at=$(api "/v1/availability?service=$service&location=$location&staff_member=$kai&from=$tomorrow&to=$tomorrow" \ | jq -r '.slots[0].starts_at')booking=$(api /v1/bookings "{\"location\": \"$location\", \"items\": [{\"service\": \"$service\"}], \"staff_member\": \"$kai\", \"starts_at\": \"$starts_at\", \"client\": {\"name\": \"Sam Patel\", \"email\": \"sam@example.test\"}}" | jq -r .id)
# The client doesn't come.api "/v1/test_helpers/bookings/$booking/force_no_show" '{}' \ | jq -r '"\(.status) \(.fee.amount) \(.fee.status)"' # no_show 2500 due
# No card was kept, so there is nothing to charge: a pay link is the way.api "/v1/bookings/$booking/charge_fee" '{}' | jq -r .error.code # no_card_on_file
# Or let it go.api "/v1/bookings/$booking/waive_fee" '{"reason": "Their first visit"}' | jq -r .fee.status # waivedimport { Tuppence } from "@tuppence/node";
const tuppence = new Tuppence(process.env.TUPPENCE_SECRET_KEY);const day = (offset) => new Date(Date.now() + offset * 86_400_000).toISOString().slice(0, 10);
// Not turning up costs the whole price.await tuppence.bookingPolicy.update({ no_show_fee: { type: "percent", value: 100 } });
// A service, someone who does it, and tomorrow's first free time with them.const [location] = (await tuppence.locations.list()).data;const session = await tuppence.services.create({ name: "Coaching session", duration_minutes: 30, amount: 2500 });const kai = await tuppence.staff.create({ name: "Kai", locations: [location.id] });await tuppence.staff.createSchedule(kai.id, { location: location.id, pattern: [Object.fromEntries(["1", "2", "3", "4", "5", "6", "7"].map((d) => [d, [["09:00", "17:00"]]]))], effective_from: day(0),});const { slots } = await tuppence.availability.query({ service: session.id, location: location.id, staff_member: kai.id, from: day(1), to: day(1),});const booking = await tuppence.bookings.create({ location: location.id, items: [{ service: session.id }], staff_member: kai.id, starts_at: slots[0].starts_at, client: { name: "Sam Patel", email: "sam@example.test" },});
// The client doesn't come.const missed = await tuppence.request("POST", `/v1/test_helpers/bookings/${booking.id}/force_no_show`, { body: {} });console.log(missed.status, missed.fee.amount, missed.fee.status); // no_show 2500 due
// No card was kept, so there is nothing to charge: a pay link is the way.try { await tuppence.request("POST", `/v1/bookings/${booking.id}/charge_fee`, { body: {} });} catch (error) { console.log(error.code); // no_card_on_file}
// Or let it go.const waived = await tuppence.request("POST", `/v1/bookings/${booking.id}/waive_fee`, { body: { reason: "Their first visit" },});console.log(waived.fee.status); // waivedimport osfrom datetime import date, timedeltafrom tuppence import InvalidRequestError, Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
# Not turning up costs the whole price.tuppence.booking_policy.update(no_show_fee={"type": "percent", "value": 100})
# A service, someone who does it, and tomorrow's first free time with them.location = tuppence.locations.list().data[0]session = tuppence.services.create(name="Coaching session", duration_minutes=30, amount=2500)kai = tuppence.staff.create(name="Kai", locations=[location.id])tuppence.staff.create_schedule( kai.id, location=location.id, pattern=[{str(day): [["09:00", "17:00"]] for day in range(1, 8)}], effective_from=date.today().isoformat(),)tomorrow = (date.today() + timedelta(days=1)).isoformat()slot = tuppence.availability.query( service=session.id, location=location.id, staff_member=kai.id, from_=tomorrow, to=tomorrow).slots[0]booking = tuppence.bookings.create( location=location.id, items=[{"service": session.id}], staff_member=kai.id, starts_at=slot.starts_at, client={"name": "Sam Patel", "email": "sam@example.test"},)
# The client doesn't come.missed = tuppence.request("POST", f"/v1/test_helpers/bookings/{booking.id}/force_no_show")print(missed["status"], missed["fee"]["amount"], missed["fee"]["status"]) # no_show 2500 due
# No card was kept, so there is nothing to charge: a pay link is the way.try: tuppence.request("POST", f"/v1/bookings/{booking.id}/charge_fee", body={})except InvalidRequestError as error: print(error.code) # no_card_on_file
# Or let it go.waived = tuppence.request( "POST", f"/v1/bookings/{booking.id}/waive_fee", body={"reason": "Their first visit"})print(waived["fee"]["status"]) # waived- Bookings reference: money for a booking: pay links, desk payments, tips, and the balance charged after the visit.
- Booking policies: every field.