Web-Nothing Docs
Guides

Customer Self-Service

Customer Self-Service: Cancel + Reschedule

Booking apps (lessons, salon appointments, restaurant deposits, classes) need a way for customers to cancel or reschedule their own bookings — without phoning the owner. Web-Nothing ships this as a built-in: configure two extra fields on your booking schema and drop one component on a public page.

This works cross-device — the customer can book on their phone, then cancel from a desktop the next day. They authenticate with their booking ID + email (the same pattern Calendly, Stripe Checkout receipts, and most restaurant booking systems use).

How it works

  1. Customer visits your public manage-booking page.
  2. They enter their booking ID and the email they used to book.
  3. The platform looks up the booking. On a match, the page shows the booking detail with Cancel and Reschedule buttons (whichever you enabled).
  4. Cancel — the platform writes your configured cancelStatusValue (typically "cancelled") into your status field. Pair this with excludeWhen.in: ["cancelled"] on your slot's availability check so the slot frees automatically for the next customer.
  5. Reschedule — the platform validates the new slot through the same conflict-check that gates new bookings (capacity, lead time, partition, stale-checkout window). The customer's own booking is excluded from the conflict scan, so they don't conflict with themselves.
  6. Both actions fire the record_updated workflow trigger automatically. Wire a send_email workflow to that trigger and the customer gets a confirmation email — no extra code.

Setup (3 pieces)

1. Add selfServiceLookup to your booking schema

{
  "name": "appointments",
  "displayName": "Appointments",
  "fields": [
    {
      "name": "slot_datetime",
      "type": "datetime",
      "availabilityCheck": {
        "capacity": 1,
        "excludeWhen": {
          "field": "payment_status",
          "in": ["cancelled"],
          "staleAfter": { "field": "payment_expires_at", "minutesPast": 5 }
        },
        "minLeadTimeMinutes": 60,
        "maxLeadTimeDays": 90
      }
    },
    { "name": "student_email", "type": "email" },
    { "name": "payment_status", "type": "string" },
    { "name": "payment_expires_at", "type": "string" }
  ],
  "selfServiceLookup": {
    "emailField": "student_email",
    "allowedActions": ["cancel", "reschedule"],
    "cancelStatusField": "payment_status",
    "cancelStatusValue": "cancelled",
    "rescheduleSlotField": "slot_datetime",
    "rescheduleAlsoUpdatesField": ["payment_expires_at"]
  }
}

Configuration keys:

KeyRequiredWhat it does
emailFieldYesSchema field that stores the customer email. Must be type: "email" or type: "string". Compare is case-insensitive + whitespace-trimmed.
allowedActionsYesSubset of ["cancel", "reschedule"]. Empty means no actions exposed (effectively turns off self-service).
cancelStatusFieldYesSchema field overwritten on cancel. Must be type: "string" or type: "enum".
cancelStatusValueYesConstant value written to cancelStatusField on cancel. Pair with excludeWhen.in: ["<this value>"] on your availability check so the slot frees.
rescheduleSlotFieldWhen "reschedule" is in allowedActionsSchema field updated on reschedule. Must be a type: "datetime" field that has availabilityCheck configured.
rescheduleAlsoUpdatesFieldOptionalUp to 5 extra schema fields that are server-stamped to the current ISO timestamp on reschedule. Typical use: ["payment_expires_at"] — see best practice below.
idFieldOptionalDefaults to "id" (auto-generated CUID). Only override if your booking schema uses a different identifier field.

2. Mark the schema as publicly visible

In the workspace publish panel (or via the platform MCP), make sure your booking schema is in publicVisibleSchemas. Without this, the lookup endpoint returns 404 to all callers (anti-enumeration).

3. Drop the manage-booking component on a public page

{
  "component": "manage-booking",
  "props": {
    "schema": "appointments",
    "title": "Manage your booking",
    "lookupCta": "Find my booking",
    "cancelCta": "Cancel booking",
    "rescheduleCta": "Reschedule",
    "successMessage": "Done — confirmation email sent shortly."
  }
}

The component handles the entire flow: lookup form → booking detail with action buttons → confirm dialog → success state. You don't wire formData, onAction, or state machines — just drop and bind.

Best practice: pair rescheduleAlsoUpdatesField with stale-checkout protection

If your booking schema uses the stale-after pattern from the booking + payment guide (excludeWhen.staleAfter releasing abandoned-checkout slots after N minutes), you should include payment_expires_at in rescheduleAlsoUpdatesField:

"rescheduleAlsoUpdatesField": ["payment_expires_at"]

Why this matters. When a customer reschedules, their booking moves to a new slot. Without the auto-stamp, payment_expires_at keeps its old value — which might already be stale. As soon as the customer reschedules into a fresh slot, the stale check would consider their record abandoned and another customer could grab the slot mid-reschedule. The auto-stamp resets the window so the customer has the full N-minute hold on the new slot.

The platform server-stamps these fields — the customer's browser cannot supply a custom payment_expires_at value. This defends against an attacker rescheduling into a far-future expiry to lock up the slot.

What the customer sees

  1. Lookup form — two text inputs (Booking ID, Email) and a Find button.
  2. After the platform finds the booking — a clean card showing the booking fields, with a Reschedule button (blue) and a Cancel button (red outline). The cancel button shows a confirm dialog before firing.
  3. After cancel — green success card.
  4. After reschedule — the customer picks a new slot from a date picker (auto-typed to datetime-local for type: "datetime" fields), and the platform's normal conflict check fires. If the new slot is taken, the customer sees a polite "this slot is no longer available" message and picks again.

Confirmation emails

Both cancel and reschedule fire the record_updated workflow trigger. Set up a send_email workflow (see Workflows) on record_updated that:

  • For cancel emails: filter where payment_status === "cancelled".
  • For reschedule emails: filter where slot_datetime changed (use record_updated_fields if your workflow supports it; otherwise fire on every record_updated and let the email template be helpful for both cases).

The customer's email address is on the booking record (student_email in the example), so the workflow already has it.

Anti-enumeration: every failure looks the same

The lookup endpoint returns identical 404 responses for ALL of:

  • The app doesn't exist.
  • The app is not public.
  • The schema isn't in publicVisibleSchemas.
  • The schema doesn't have selfServiceLookup configured.
  • The booking ID doesn't exist.
  • The booking ID exists, but the email is wrong.

This is deliberate — it prevents an attacker from probing whether a specific booking ID exists in your app. Customers who type their email incorrectly see a friendly "we couldn't find a booking matching that ID and email" message regardless of which gate failed.

Security model

  • Token-based action authentication. After a successful lookup, the platform issues a short-lived signed token (default 30 minutes). The token binds the app, schema, and booking ID — it can't be replayed against a different record.
  • Token never appears in the URL. It's held in browser memory and sent only in the JSON body of action requests. No browser-history leak, no referrer leak, no access-log leak.
  • Rate limits. Lookup is capped at 30 requests per hour per IP per app (anti-enumeration). Cancel/reschedule actions are capped at 10 per hour per IP per app (write protection).
  • Audit log. Every lookup and action attempt is logged with SHA-256-hashed booking ID and email (no plaintext PII), plus the first three octets of the caller's IP address for rough geo-binning.
  • Cookie filter bypass. The customer-session cookie filter (customerScoped) is intentionally bypassed for self-service — the booking ID + email is the auth signal, replacing the cookie. This is what lets a customer book on their phone and cancel from a desktop the next day.

Common mistakes to avoid

  • Don't forget excludeWhen.in: [cancelStatusValue] on the slot's availabilityCheck. Without it, cancelled bookings still block the slot. The cancelStatusValue you configure for self-service must appear in this list.
  • Don't use customerScoped: true AND expect cross-device cancel to work without self-service. The cookie filter is browser-bound; only self-service tokens unlock cross-device. Self-service is the correct pattern when customers might cancel from a different device than they booked from.
  • Don't make the email field free-text without validation. If customers can type anything in the email field at booking time, lookup will fail when they get the email subtly wrong (extra space, typo). Use type: "email" so the platform validates at booking time.
  • Don't expose internal status fields via the cancel path. The cancelStatusValue you set must be one your downstream workflows / reports recognize — it's not a private internal flag.

Limitations and Phase 2

  • Magic-link tokens in confirmation emails — pre-fill the lookup page via a URL token so the customer doesn't re-type booking ID + email — is a future enhancement coming in a future release. Don't roll your own email-token mechanism in the meantime; you'd duplicate what's coming.
  • Auto-refund on cancel — refunds remain an owner ops decision via the Stripe Dashboard. The platform doesn't trigger refunds automatically.
  • Reschedule fees — charging the customer to reschedule (e.g. "$10 if rescheduling within 24 hours") is not built in; you can express the rule via minLeadTimeMinutes to reject the reschedule entirely or handle billing in your own workflow.
  • Customer-side audit log — a "you cancelled this booking on date X from IP Y" view for the customer is not built in.
  • Email change recovery — if the customer's email changed after booking, they cannot self-cancel; they must phone the owner who cancels via the workspace.

See also

On this page