Bookings
The platform's keystone Basic module. Services, weekly availability with multiple ranges per day, off days, discount codes, the public booking page, Stripe payment, calendar invites, the bookings dashboard, the reschedule and cancel flows, manual bookings, the link to Meeting Notes, and how to keep Stripe + your calendar healthy.
Download PDFOverview
Bookings is the platform's first and most-used module. It carries everything you need to operate a public-facing scheduling page: the services you offer, your weekly availability, the off days when you are not bookable, the discount codes you hand out for marketing campaigns, the bookings themselves as they come in, and the lifecycle of each booking from the moment a prospect picks a time through the post-meeting Meeting Notes. The module is open to every tenant on every tier (Basic and Premium); a few sub-features unlock additional behaviour on Premium (the AI document summary when a prospect uploads documents at booking time, runtime translation of the public page into the eight non-EN-FR languages, the post-meeting AI Populate inside Meeting Notes).
What the public booking page is
Every tenant gets a public URL at /book/<your slug> where prospects pick a service, see your real-time availability, choose a date and a time slot, fill in their contact details, optionally pay through Stripe Checkout, and receive an instant confirmation email plus a calendar invite. The page is fully self-serve. You set it up once (services + availability + Stripe + calendar) and the rest happens without your intervention: prospects book on their own schedule, the calendar event lands on yours, the dashboard list updates, the meeting reminders fire, and after the meeting you click into the booking row to capture Meeting Notes.
The four configuration cards
Services: every service you offer (a 30-minute consultation, a 60-minute follow-up, a free initial chat, a paid in-person sit-down, etc.). Each service carries a name, a duration, a price, a meeting mode (online / in-person / both), and a few additional knobs. See the Services section below.
Availability: the days of the week and the hours within each day when you are available for bookings. You can have multiple time ranges in the same day (8 AM to 10 AM AND 2 PM to 6 PM, for example). See the Availability section below.
Off Days: specific dates blocked off (a holiday, a vacation week, a sick day) regardless of your weekly availability. See the Off Days section below.
Discounts: promo codes prospects can enter on the booking form to get a percentage or fixed-amount discount on a service. See the Discounts section below.
Important: Two prerequisites unlock the full module: connecting Stripe (so prospects can pay for paid services) and connecting at least one calendar (so the platform knows when you are busy outside the platform). The next two sections walk you through both. You CAN run the platform with neither connected (free services only, availability driven solely by existing in-app bookings) but the experience is noticeably more limited.
Before you start: connecting Stripe (Standard Connect)
Stripe is the only way to collect online payment for a paid booking. We use Stripe Connect with the Standard account type: a Stripe account YOU own, registered to your business, with funds settling directly to YOUR bank account. We never hold your money; the platform takes a small per-transaction commission via Stripe's application fee mechanism and Stripe routes the rest to you on Stripe's normal payout schedule (next-business-day in Canada, with a 2-day delay on your first few payouts as Stripe's risk team gets comfortable with your account).
How to connect Stripe
Open Settings from the top-right gear icon. Find the Payments card. Click 'Connect Stripe'.
You are redirected to Stripe's hosted onboarding (connect.stripe.com). If you have an existing Stripe account, sign in with that account; if not, Stripe walks you through creating a new account: business name and address, the named legal representative, your bank account for payouts, identity verification (driver's licence or passport upload), and your sector classification (pick 'Legal services' if Stripe asks; immigration consulting falls under that taxonomy on Stripe's side).
When Stripe finishes its onboarding flow, it redirects you back to our Settings page. The Payments card now shows your Stripe account email, a green Connected badge, and a 'Manage on Stripe' link (which deep-links you back to your Stripe dashboard for the payout schedule, tax forms, dispute management, and refund history).
Until your Stripe account is verified, Stripe holds payouts on Test mode for some accounts. New tenants typically see their first payout within 7 business days after the first successful booking. If you do not see a payout after that window, click 'Manage on Stripe' and check the Stripe dashboard's home page for any open verification requests.
How the platform fee works
On Basic tier, the platform takes a tiered commission via Stripe's application_fee_amount: 1% on the first $5,000 CAD of monthly Stripe-processed volume, 0% above that cap. Premium tier pays 0% platform fee on every transaction. The commission is taken at the moment Stripe captures the payment; you see the gross amount in your Stripe dashboard, then the platform fee as a separate line, then the net amount payouts to your bank account. The cap resets monthly on the first of the month in your tenant timezone. Per-tenant overrides exist (an Investatech-granted reduced rate for partnership tenants) and would show on your /admin profile if applicable.
Without Stripe connected
Free services (price = $0) still work fully. Prospects book, you confirm, the calendar invite goes out. No payment step.
Paid services are HIDDEN from the public booking page. The form simply does not list them. Prospects see only your free services. This is the platform's defense against creating a booking with no clear payment path.
An amber 'Stripe not connected' banner renders on your Services page reminding you to connect.
Existing paid bookings (from before a Stripe disconnect, if you ever disconnect and reconnect) keep their payment history; refunds on those bookings continue to work through the original Stripe account.
Important: Self-serve Stripe disconnect is deliberately NOT a tenant-facing feature. In-flight refunds, disputes, and late webhooks bind to the original Stripe account.id, and they break after a swap. If you genuinely need to change Stripe accounts (a corporate restructure, for example), email info@investatech.com and we will coordinate an operator-handled SQL migration that preserves your bookings + payment history.
Before you start: connecting a calendar
A connected calendar serves two purposes. First, READ direction: the platform reads your calendar's freebusy intervals to subtract any time you are already busy from the public booking page's available-slots calculation. Without a calendar connection, prospects could book a slot that conflicts with a meeting you have outside the platform. Second, WRITE direction: every booking we accept creates a real calendar event on your calendar with the prospect attached as an attendee, the meeting mode + location, and (for online meetings on Google Calendar) an auto-generated Google Meet link. The calendar invite arrives in your inbox the moment the booking confirms, and the prospect gets the same invite.
Two providers, up to two slots per member
Each member can connect up to TWO calendars on their My Profile page (the silhouette icon in the top-right). The two slots are PRIMARY and SECONDARY. The primary slot is where bookings WRITE events (so the prospect's invite lands on this calendar and any Google Meet auto-creation runs on this calendar). The secondary slot contributes its busy intervals to the read direction but does NOT receive new bookings. You can mix providers: primary Google Calendar + secondary Outlook Calendar, primary Outlook + secondary Google, two Google calendars, two Outlook calendars. The 'Make primary' button on a connected secondary swaps the two slots without losing tokens.
How to connect Google Calendar
Open My Profile (the silhouette icon in the top-right). Find the Calendars card. Click 'Connect Google Calendar'.
You are redirected to Google's OAuth consent screen at accounts.google.com. Sign in with the Google account whose calendar you want to use. Google shows you the permissions we are asking for: read your calendar's freebusy info, create events on your calendar, send invitations to attendees, attach Google Meet links to those events when you ask for one, and read your account email for identification. Click Allow.
Google redirects you back to our My Profile page. The Calendars card now shows your Google account email, a green Connected badge, the connect timestamp, and three buttons: Disconnect, Make primary (when connected as secondary), and Reconnect (when the connection has gone stale and needs a fresh OAuth round-trip).
How to connect Microsoft 365 (Outlook Calendar)
Same path: My Profile -> Calendars card -> 'Connect Outlook Calendar'.
You are redirected to Microsoft's OAuth consent screen at login.microsoftonline.com. Sign in with the Microsoft 365 account whose calendar you want to use. The permissions we ask for are Calendars.ReadWrite (read freebusy + create + update + delete events) plus User.Read (for your account email) plus offline_access (so we can refresh the token without prompting you every hour). For tenants with a Teams for Business license, the platform also asks for Teams meeting creation so online meetings get auto-attached a Teams link the same way Google Meet works on Google Calendar; for personal Outlook accounts or Microsoft 365 tenants without Teams Business, the calendar-create step degrades gracefully (the event is created, the Teams link is omitted).
Microsoft redirects you back to our My Profile page. The Calendars card shows your Microsoft account email and the same three management buttons.
Without a calendar connected
The platform still functions. Availability calculation falls back to the in-app data only: your weekly availability minus your existing confirmed bookings minus your off days. The platform does NOT know about any meetings on your calendar that originated outside the platform, so a prospect could book a slot during, say, your previously-scheduled court appearance that lives on your Google Calendar but never reached our database. You can absorb the conflict manually (reschedule the prospect, or reschedule the conflicting external meeting) but a connected calendar removes the problem at the source.
Note: If your calendar connection ever goes stale (Google or Microsoft expired the refresh token, which can happen after a long period of inactivity or after a security event on your Google or Microsoft account), the platform marks the connection as reauth_required and renders an amber 'Reconnect' card on My Profile and on every booking surface. Bookings still flow through (we degrade to read-only for the affected member's slot) but you should click Reconnect at your next convenience.
Services: overview
Services are the unit of booking. Every booking is a booking of one service for one date and time. A service carries the name a prospect sees on the public page, the duration of the meeting, the price (if any), the meeting mode (online / in-person / both), the languages spoken, whether a CICC consultation agreement is required, the popular flag, and whether a single booking can carry multiple attendees. You can have as many services as you need; the public page lists every active service in a picker. Inactive services do not appear on the public page but stay in your dashboard for historical reference and to keep existing bookings linked.
Where the Services page lives
Open the Bookings module from the sidebar. The sub-navigation carries Bookings (the list of bookings), Services, Availability, Off Days, and Discounts. Click Services. The Services page title reads 'Services' with the subtitle 'Manage the services your clients can book'. A maroon '+ Add Service' button sits in the top-right. Below the header, every existing service renders as a card with its name, status pills (Popular / Attendees / CICC*Agreement / Online / In-person / Both), the duration and price summary line, and three actions: Deactivate (or Activate when already inactive), Edit (pencil icon), Delete (trash icon).
Status pills on a service card
Popular: a maroon pill marking the service you have flagged as your most-asked or most-recommended option. Renders as a maroon star ribbon on the public booking page picker. You can have at most ONE Popular service at a time (a partial unique index enforces this); marking a second service Popular clears the flag on the previous one.
+ Attendees: this service accepts multiple attendees per booking (a workshop, a group consultation, a family-meeting setup). The public booking form shows a 'How many attendees?' field; capacity rules apply (see services-attendees below).
CICC*Agreement: a CICC-aligned consultation agreement is generated and the prospect signs it on the booking page before the booking confirms. The asterisk pairs with the platform-wide Independence-of-CICC footer disclaimer. See the services-cicc-agreement section below.
Online / In-person / Both: the meeting mode pill. Online services auto-attach a Google Meet (Google Calendar) or Teams (Microsoft 365 with Teams Business) link; in-person services prompt the prospect to confirm they can reach the office address; Both lets the prospect choose at booking time.
Services: format (live, written, Q&A)
Every service carries a 'format' field that determines what kind of engagement it is. Three formats are supported:
Live (the default): a real-time meeting between you and the prospect at a scheduled date and time, via video conference or in person. This is the canonical Bookings flow described in this entire chapter. Every behaviour in this chapter assumes Live unless noted.
Written: a written-advice ticket where the prospect submits questions in writing and you respond in writing, with a tiered cap on page count and turnaround. Written services are managed in the Written Consultations module; they show up in the Services list for visibility but the lifecycle lives in /dashboard/written-consultations. See the Written Consultations chapter for details.
Q&A: a shared-session-per-date Q&A block where multiple prospects can register for the same date and the first registrant sets the start time. The lifecycle lives in /dashboard/q-and-a. See the Q&A chapter for details. Q&A services have additional fields (available days of week, session language, capacity max, tenant-notify toggle).
Note: When you create a new service via the Add Service button, the format defaults to Live. Changing format on an existing service is restricted: switching FROM Live to Written or Q&A clears live-only fields (the meeting mode, the duration block) and prompts you to fill in the format-specific fields. Switching back to Live clears the written-only or Q&A-only fields. Existing bookings are preserved either way; only future bookings adopt the new format.
Services: the Add / Edit form, field by field
Click '+ Add Service' on the Services page. A side panel opens with the full service form. Editing an existing service opens the same form pre-filled. Here is every field you will encounter, in order.
Service name (required)
The headline that appears on the public booking page picker and on the calendar event title. Keep it concise and descriptive: '30 minute consultation', 'Initial intake (free)', 'Follow-up call (60 min, paid)', 'In-person sit-down at the Toronto office'. Three to seven words is usually enough; the picker truncates very long names. The name is bilingual on the public page: Premium tenants get runtime translation, but the canonical name you type here is the source of truth.
Short description (optional)
A one or two-sentence description that renders under the service name on the public picker. Use it to set expectations: 'A focused 30-minute discussion of your case strategy and next steps' or 'Free 15-minute introduction call to see if we are the right fit for your matter'. The description is shown alongside the price and duration; an empty description is fine but a thoughtful one converts better.
Duration (required)
The length of the meeting in MINUTES. Typical values: 15, 30, 45, 60, 90, 120. The picker on the public page uses the duration to compute the slot grid: a 30-minute service on a Monday with availability from 12 PM to 5 PM produces ten 30-minute slots (12:00, 12:15, 12:30, ..., 4:30; the platform also offers 15-minute increments WITHIN your availability window so a prospect can pick 12:15 PM and the meeting runs 12:15 to 12:45). The duration also drives a 15-minute buffer the platform applies around every confirmed booking when computing the next prospect's availability, so back-to-back bookings have breathing room.
Price (required)
The dollar amount the prospect pays for this service. Type 0 (zero) for a free service. The platform stores the value internally in CENTS (so $50.00 becomes 5000 cents on the database row) but the form accepts dollars-and-cents input ('50' for $50.00, '49.99' for $49.99). The money-input pattern (Rule 41 internally) means typing works without the .toFixed(2) caps-at-last-format trap. A price greater than zero requires Stripe to be connected; if Stripe is not connected, the form refuses to save a paid service with an inline 'Connect Stripe first' notice.
Currency (locked to your tenant currency)
Every service uses your tenant's configured currency, which defaults to CAD. Multi-currency on services is not supported in v1; if your practice operates across currencies, you typically convert to CAD for invoicing purposes (which is also how the Stripe application_fee_amount computes). The currency code is shown next to the price on the public picker (CA$50.00 for example) so prospects know what they are paying in.
Meeting mode (required: Online / In-person / Both)
Online: the meeting happens via video conference. If you have Google Calendar connected, the platform auto-attaches a Google Meet link to every calendar event for this service. If you have Microsoft 365 with Teams Business, the platform auto-attaches a Teams link. The link lands in both your calendar invite AND the prospect's confirmation email.
In-person: the meeting happens at a physical address. Picking In-person reveals a Location address field below where you type the office street address. The address appears on the calendar event location and on the prospect's confirmation email. The platform does NOT auto-attach a video link for in-person services.
Both: the public booking form offers the prospect a radio choice between Online and In-person at booking time. The platform applies the right calendar-event behaviour based on the prospect's choice.
Languages spoken (optional)
A multi-select that lists the languages you can hold this meeting in. The public picker renders the languages as small chips under the service description so prospects know whether to book with you for a Mandarin matter, a Spanish matter, an Arabic matter, etc. The default is English and Quebec French; on Premium with Phase 10C runtime translation enabled, the picker offers eight additional languages (Spanish, Mandarin, Tagalog, Punjabi, Arabic, Hindi, Urdu, Persian / Farsi). The languages field does not enforce; it is informational. Prospects can still book even if they speak a language you have not listed.
CICC consultation agreement required (toggle)
When this toggle is on, the platform generates a CICC-aligned consultation agreement for the booking and the prospect must sign it on the booking page before the booking confirms. The agreement is rendered in real time from your tenant's settings: your name and CICC registration number, the matter scope inferred from the service, the consultation-only nature of the engagement (this is NOT a retainer agreement), the confidentiality and fee disclosure, the file-retention period, and the Code section 24 disclosures applicable to a pre-engagement consultation. The toggle is GREYED OUT until at least one team member on your tenant has a verified RCIC attestation (set on My Profile -> Regulated Consultant). The agreement is signed and stored alongside the booking; you find it on the booking row via the 'View signed agreement' link.
Multi-attendee (toggle: '+ Attendees')
When on, the booking form lets a single registrant book the service for multiple attendees (a couple seeking joint advice on a spousal sponsorship, a family of four discussing a study permit for a child, a small business with two co-owners meeting about an LMIA). A 'How many attendees?' field appears, capped at a max-attendees-per-booking number you configure on the same form. The booking row in your dashboard shows the attendee count. Each attendee can have a separate name + email captured on the booking form (the platform then sends every attendee their own confirmation email and adds each one to the calendar event as an attendee). For paid services, the price is per-BOOKING (not per-attendee) unless you set up multi-attendee pricing explicitly.
Popular (toggle, max 1 per tenant)
Mark this service as your most-recommended option. The public picker renders a maroon star ribbon on the service card and a 'Most popular' label. Use it to steer prospects toward the offering you want them to land on first (typically your headline paid consultation or your headline initial intake). Only ONE service can be Popular at a time; the database partial unique index enforces this. If you click the toggle on a second service, the platform clears the flag on the previous one and you see a brief 'Popular flag moved to <new service>' toast.
Assigned member (Team Mode only)
When your tenant is on Team Mode (Premium feature where you have multiple team members beyond yourself), each service can be assigned to a SPECIFIC member or left as Shared. Shared services accept bookings against any team member's availability; member-assigned services route bookings only to that member's calendar and only show that member's availability on the public picker. Use member-assigned services when you want, for example, your assistant to take all initial intake calls while you (the RCIC) take only the paid follow-ups. On a solo tenant (the default), the field is hidden; every service is effectively assigned to you.
Save vs Save and add another
Two buttons at the bottom of the form. Save closes the side panel and returns to the Services page with the new service visible. Save and add another saves the current service and immediately opens a blank form for the next one; useful when you are setting up your first set of services at once.
Services: the CICC consultation agreement
The CICC-aligned consultation agreement is the platform's compliance backbone for pre-engagement consultations. CICC Code of Professional Conduct section 24 requires a written agreement before a licensee performs services for compensation; even a paid 30-minute consultation falls under this requirement. The platform's auto-generated consultation agreement satisfies that requirement for the consultation itself (note that this is the CONSULTATION agreement, distinct from the full retainer Service Agreement that follows if the prospect engages you for the underlying matter; for the retainer agreement, see the Service Agreements module).
When the agreement renders
On the public booking form, after the prospect has picked a date and time and entered their contact details, if the service has CICC*Agreement turned on, a new section unfolds with the rendered consultation agreement text. The prospect reads, then ticks an 'I have read and understood the above agreement' checkbox plus a 'Sign by typing my full name below' text field where they type their name as their electronic signature. The platform generates a PDF of the signed agreement, encrypts it under your tenant DEK, and stores it. The PDF is attached to the prospect's confirmation email and is downloadable from the booking row in your dashboard.
What the agreement contains
Identification block: your tenant business name, the named RCIC (from the service's assigned member, or the Owner-member fallback when shared), their CICC registration number, the prospect's name + email + phone + address.
Engagement clause: framing the consultation as a pre-engagement discussion, not a retainer. The licensee gives general advice and information; specific application advice or representation requires a separate Service Agreement.
Scope clause: covers the matter category the prospect indicated (study, work, visit, sponsorship, etc., from the booking form's matter intake field) and explicitly excludes everything else.
Fee clause: the consultation fee (the service price). For free consultations, the clause renders as 'There is no fee for this consultation.'
Confidentiality clause: covers the licensee's obligation to keep the consultation content confidential, the file-retention period (default seven years), and the regulator's right to inspect under Code section 26.
No-guarantee clause: the licensee does not guarantee any specific outcome on the prospect's matter (Code section 24 specifically prohibits outcome guarantees).
Complaint clause: how the prospect raises concerns with the licensee first, how they file a complaint with CICC if unresolved, with the College's website URL.
Signature block: the prospect's typed name + the booking timestamp (UTC) + the booking reference number + the prospect's IP address (truncated to /24 CIDR for the audit footer). The licensee's electronic counter-signature is auto-applied from the saved signature on the RCIC's company_members row.
Important: The verified-RCIC gate is mandatory for CICC*Agreement to even be available as a toggle on the Service form. Without at least one team member having completed the Phase 33 RCIC attestation (My Profile -> Regulated Consultant card -> upload a screenshot of your CICC public register page and check the attestation box), the platform refuses to render the toggle. This is a Code section 24 compliance posture: a tenant who has not demonstrated they have a regulated person on staff cannot use a regulated-person-required instrument.
Services: multi-attendee bookings
A single booking can carry multiple attendees when the service has the '+ Attendees' toggle on. The form on the public side then renders a 'How many attendees?' numeric input plus N rows of name + email fields (one row per attendee, dynamically). The platform stores attendees on a sibling event_attendees table; the booking itself records the registrant (the primary contact), and each attendee row has its own ticket reference. The calendar event includes every attendee as a Google Calendar attendee (or Outlook attendee) so each one gets an invite directly in their inbox.
Capacity cap per service
When you turn the toggle on, a 'Max attendees per booking' field appears. Sensible defaults: 4 for a couples' or small-family consultation, 10 for a business meeting, 25 for a workshop. The platform enforces the cap on the public form (the numeric input clamps at the max) AND on the server-side validator (a malicious request that hand-rolls a higher count is rejected with a 400). If a booking is made for the max capacity, subsequent prospects see a 'this time slot is fully booked' message; you can still manually add bookings to that slot from the dashboard if you choose to overbook, but the public form refuses.
How attendees show on your dashboard
On the Bookings list, a multi-attendee booking shows the primary contact name plus a chip 'N attendees'. Clicking into the booking detail panel reveals every attendee as its own row with name + email + ticket reference + their own per-attendee 'attended / no-show' marker. The calendar event also lists every attendee as RSVP'd; you can see who actually showed up on the video call by checking who is on the call vs the attendee list. The Meeting Notes flow (the Notes button on the booking row) lets you capture notes per the booking as a whole, NOT per attendee; if you need per-attendee notes (a couples consultation where each spouse needs their own follow-up), make a separate Meeting Notes row by clicking Notes on the same booking once per attendee context you want to capture.
Note: Multi-attendee bookings are NOT the same as the Events module. Events are public group sessions where MULTIPLE registrations (each with their own payment) accumulate into one calendar event (typical: a workshop where 20 separate prospects each pay separately and all attend together). Multi-attendee bookings are ONE registration with multiple attendees under it (typical: a couple booking jointly). When in doubt: 'they are paying together as a unit' is a multi-attendee booking; 'they are paying separately for the same session' is an Event.
Services: deactivate, activate, and delete
Each service card carries three management actions on the right: Deactivate (or Activate when already inactive), the pencil-icon Edit button, and the trash-icon Delete button. The distinction between Deactivate and Delete matters more than it looks.
Deactivate
Removes the service from the public booking page picker. New prospects cannot book it.
Preserves every existing booking that referenced this service. Bookings stay confirmed; calendar events stay in place; refunds on past bookings still work.
The service card stays in the Services list, just with an amber 'Inactive' badge instead of the green status pills.
Use Deactivate when you are taking a service offline temporarily (a seasonal pause, a service you no longer offer but want to preserve in history, a service mid-redesign where you do not want bookings while you adjust it).
Reverse with Activate, which restores the green status pills and the public picker visibility.
Delete
Hard-delete the service row from the database. Only allowed when ZERO bookings reference this service. The delete button is greyed out (with a tooltip explaining why) on any service that has at least one historical booking.
Confirms with a small modal: 'Delete <service name>? This cannot be undone.' Click Delete to confirm.
Use Delete only on test services you created during onboarding (before any real bookings landed) or on a never-booked service you want to remove entirely. For any service with bookings, Deactivate is the right action.
Availability: overview
Availability is your weekly schedule of when prospects can book. It is a repeating pattern: 'every Monday from 12 PM to 5 PM', 'every Tuesday from 8 AM to 10 AM AND from 2 PM to 6 PM', 'every Thursday from 9 AM to 1 PM'. The pattern repeats indefinitely; new bookings on next-month Monday between 12 PM and 5 PM are valid because the Monday-12-to-5 rule is still in force. The public booking page reads your availability + subtracts your existing bookings + subtracts your calendar freebusy + subtracts your off days, then renders the remaining slots in the picker.
Where the Availability page lives
Bookings module sidebar -> Availability. The page title reads 'Availability' with the subtitle 'Set when you are available for bookings' and a small help icon. A maroon '+ Add Time Block' button sits in the top-right. Below the header, on Team Mode tenants, a 'Viewing:' dropdown lets you filter the displayed availability to All members or a specific member. Below that, seven cards (one per day of the week, Sunday through Saturday in fr-CA order or whichever order matches the dashboard locale) list every time range you have configured for that day.
How the day cards read
A day with no time ranges configured reads 'Unavailable' on the right (charcoal text). Prospects cannot book that day; the public picker hides that weekday from the date calendar entirely.
A day with one time range reads as a single row: '12:00 PM to 5:00 PM' plus the member chip (on Team Mode; 'John Doe' for example) plus a Disable button + trash icon for inline removal.
A day with multiple time ranges reads as multiple rows stacked vertically: '8:00 AM to 10:00 AM' then '2:00 PM to 6:00 PM' on the same card. Each row has its own member chip + Disable + trash. This is THE pattern for split-day schedules; see the next section for details.
Availability: more than one time range in the same day
Note: This is the most-asked Bookings configuration question. The answer: yes, you can have any number of time ranges in the same day. The platform treats each range as an independent row; the public picker offers slots within ANY of the ranges as bookable.
Why split a day
The most common reasons. First, lunch breaks: a typical 9-to-5 day becomes 9-to-12 plus 1-to-5 so the noon hour is yours for lunch and prospects cannot book then. Second, school pickup: a parent licensee blocks the 3-to-4 PM slot daily by configuring 9-to-3 plus 4-to-6. Third, court appearances on specific days: 8-to-10 morning intake calls then 2-to-6 afternoon follow-ups, with the 10-to-2 block being your court time on every Tuesday. Fourth, intentional buffer: 9-to-11 morning slots and 1-to-3 afternoon slots, with the 11-to-1 block deliberately reserved for prep and admin work even on days with light external commitments. Fifth, split-shift practice (early morning + evening to accommodate international clients): 7-to-9 AM and 7-to-9 PM with a long break in between.
How to add a second time range to the same day
Click '+ Add Time Block' in the top-right of the Availability page. A side panel opens with the time-block form.
Pick the day of the week. The dropdown lists Sunday through Saturday. Pick the day you want the second range on (say, Tuesday).
Pick the start time. The picker accepts any 15-minute increment from 00:00 to 23:45. For your afternoon block, type or pick 14:00 (2 PM).
Pick the end time. Same 15-minute granularity, must be strictly after the start. For your afternoon block, type or pick 18:00 (6 PM).
On Team Mode, pick which member this range is for from the Member dropdown. On solo tenants, the dropdown is hidden; the range applies to you implicitly.
Click Save. The side panel closes; the Tuesday card now shows TWO rows: the original morning range AND your new afternoon range. Both are independent; you can disable or remove either without affecting the other.
How the public picker handles split ranges
When a prospect picks Tuesday on the date calendar of the public booking page, the picker computes slots by iterating BOTH ranges. Within range 1 (8 AM to 10 AM), it generates 15-minute slot starts: 8:00, 8:15, 8:30, ..., 9:45 (the last possible 15-minute start for a 15-minute duration). Within range 2 (2 PM to 6 PM): 14:00, 14:15, 14:30, ..., 17:45. The two slot groups are concatenated; the picker renders all of them as a single scrollable list of available times, with the lunch block (10 AM to 2 PM) absent from the list entirely. Prospects see a natural-looking schedule without ever having to understand that you split the day; they just see your available hours.
Overlap is rejected
If you try to add a range that overlaps an existing range on the same day (8 AM to 12 PM when 10 AM to 11 AM already exists), the form refuses with an inline 'this range overlaps an existing range on Tuesday' error. The two ranges must be non-overlapping. The platform does not auto-merge; if you want 8 AM to 12 PM, delete the 10-to-11 range first and add the wider one. This guards against an accidental duplicate that would compute the same slot twice in the public picker.
Note: Times are wall-clock in your tenant timezone. The platform handles daylight saving time transitions automatically: a 9 AM range on a Sunday-DST-spring-forward week is still 9 AM in your local clock, even though the underlying UTC offset shifted. You never need to re-edit availability for DST.
Availability: per-member ranges (Team Mode)
When your tenant is on Team Mode (Premium feature with multiple team members), every availability time range is OWNED by exactly one team member. The owner's name appears as a chip next to the range on the day card ('John Doe' badge for example). The 'Viewing:' dropdown at the top of the page filters: All members shows every range across the team stacked together; picking a specific member shows only that member's ranges.
How the public picker handles team availability
When a prospect picks a SHARED service (any service whose 'Assigned member' field is unset), the public picker computes availability by UNIONING every team member's ranges for the picked date. A slot is available if AT LEAST ONE team member has the slot free. The platform then routes the actual booking to the FIRST available team member (deterministic round-robin within the slot). When a prospect picks a MEMBER-ASSIGNED service (the Assigned member field is set to a specific person), the picker uses only that one member's ranges; the booking routes to that member regardless of who else might be free.
Per-member URLs for direct booking
Each team member also gets their own per-member URL at /book/<tenant-slug>/<member-slug>. The member slug is auto-generated from the member's full name (john-doe for John Doe). Visiting this URL bypasses the 'pick a service' step's team aggregation and constrains the picker to only that member's availability. Useful when you want to email a specific prospect a link 'book directly with John Doe at this URL'; the prospect lands on a member-scoped picker that does not invite confusion across the team.
Availability: with calendar freebusy merge
When you have a calendar connected (Google Calendar or Microsoft 365 OneDrive Calendar), the public picker's availability calculation does NOT stop at your weekly ranges. The picker also calls the calendar provider's freebusy API for the picked date and SUBTRACTS every busy interval from your weekly ranges before generating slots. The result: a slot you have a meeting on (a court appearance, a personal commitment, a meeting you took outside the platform) is HIDDEN from the prospect's view.
Dual-calendar freebusy
When a team member has TWO calendars connected (primary + secondary, mixing Google + Microsoft is fine), the platform queries freebusy from BOTH calendars and unions the busy intervals. A meeting on your secondary calendar blocks the slot in the public picker even though new bookings will only write to your primary calendar. Useful when one calendar is your work calendar (where bookings should land) and the other is your personal calendar (whose busy time should still block bookings).
Batched freebusy on the date picker
When the prospect opens the date picker for, say, the next 30 days, the platform makes ONE batched freebusy call covering the full date range (rather than 30 separate per-date calls). The picker pre-computes available-count per day and renders the date calendar with greyed-out days that have zero available slots and visible days that have at least one. The prospect picks a date with available slots, the picker renders the slot list using already-fetched freebusy data, no extra round-trip. This keeps the public page fast even on tenants with full calendars.
What kind of events block slots
Regular events with transparency=opaque (the calendar default for a meeting). These block.
Free / transparent events (transparency=transparent). Skipped; these do not block. Use this when you want to mark a calendar event as 'on my calendar for reference but I am still bookable' (a casual reminder, a deadline-only event, a tentative hold you have not accepted).
All-day events. Skipped on Google freebusy (Google's freebusy API returns all-day events as busy only when they are marked busy). Inspect on your end if a prospect books over your vacation day; that signal usually means you marked the all-day event as transparent. Off Days (covered next) are the safer way to block whole days.
Declined events. Skipped on Google + Microsoft. A meeting you were invited to but declined does not block.
Tentative events. Treated as busy by default on Google + Microsoft. If you have not accepted a meeting yet, it still blocks the slot in the public picker. Accept or decline to disambiguate.
Availability: without a calendar connected
If no team member on the relevant routing has a calendar connected, availability falls back to the in-app data only. The picker subtracts your existing confirmed bookings on the platform plus your off days from your weekly availability ranges, generates slots from what remains. The 15-minute buffer around existing bookings still applies. The result is correct as far as platform-originated bookings go, but the platform has zero visibility into your external meetings.
What this means in practice
A prospect can book a slot during your previously-scheduled court appearance that lives on your unconnected Google Calendar. The platform did not know; it accepted the booking.
You see the conflict in your dashboard the next time you check it (the booking is in your dashboard and the calendar event is on your calendar but they are not the same slot, they are at the same time).
You absorb the conflict manually: reschedule the prospect to a later slot via the booking detail panel's Reschedule action (which emails the prospect a fresh time), or reschedule the conflicting external meeting on your own calendar.
Connect a calendar as soon as practical; the manual conflict resolution is genuine work that the platform can eliminate.
Important: On a Team Mode tenant, if SOME team members have calendars connected and others do not, the picker's behaviour depends on the service. A shared service unions everyone's availability; the unconnected member's ranges contribute slots that ignore their external commitments. A member-assigned service that points at an unconnected member behaves as the solo case above. To avoid surprise conflicts in a team setup, make sure every member who takes bookings has at least one calendar connected.
Off Days: overview
Off Days are specific dates blocked off REGARDLESS of your weekly availability. You configure them at Bookings sidebar -> Off Days. The page title reads 'Off Days' with the subtitle 'Block specific dates so clients cannot book them, holidays, travel, sick days'. A maroon '+ Block a Date' button sits in the top-right. The page lists Upcoming off days first (a chronological list of every future off day), then Past off days collapsed below the fold.
What an Off Day does
Hides the off day from the public booking page's date picker. The day is rendered greyed out with no slots; prospects cannot pick it.
Does NOT affect calendar events you already have on that date. If you had bookings already on the calendar for a day you later mark as Off, those bookings stay confirmed (you cancel them manually if you want). Off Days are a forward-looking gate, not a retroactive purge.
Repeating an Off Day every year is not automatic. Statutory holidays (Canada Day, Christmas, Labour Day) need to be re-entered each year, because the platform does not assume your jurisdiction's holiday calendar. A future enhancement will add a 'block every <date> annually' option; for now, plan your annual off days as a year-start admin task.
Off Days: the Add / Block form
Click '+ Block a Date'. A small modal opens with three fields: the start date (a date picker), the end date (a date picker, must be on or after start), and an optional reason field (a free-text label that appears on the off-day card; useful for your own reference, never shown to prospects).
Single-day vs multi-day blocks
Single-day: pick the same date for start and end. Examples: 'Canada Day 2026' on 2026-07-01, 'sick day' on 2026-03-15.
Multi-day: pick different start and end dates; every day in the range (inclusive) is blocked. Examples: 'Vacation week' from 2026-08-04 to 2026-08-11, 'Conference attendance' from 2026-10-20 to 2026-10-23.
Reason field
Optional but recommended. The reason renders on the off-day card so a teammate (on Team Mode) or future-you scrolling through past off days knows why this date was blocked. Examples: 'Personal vacation', 'Court hearing all day', 'Statutory holiday (Canada Day)', 'Doctor appointment'. Prospects NEVER see this reason; the public picker just sees the day as greyed out with no explanation. Treat the reason as internal notes.
Editing or removing an Off Day
Each off-day card on the page carries a trash icon to remove it. There is no inline edit; if you need to adjust the date range, remove the card and add a fresh one. Removing an off-day makes the date(s) bookable again on the public picker, subject to your weekly availability and calendar freebusy for those dates.
Off Days: per-member vs all-members scope (Team Mode)
On Team Mode tenants, the Off Days page surfaces a 'Viewing:' dropdown filter (All members / specific member) just like the Availability page. The Add form also gains a 'Whose off day?' dropdown: pick a specific team member (this date is THEIR off day, the rest of the team is still bookable) or pick 'All members' (the date is blocked for everyone, useful for company-wide holidays like Canada Day).
Interaction with shared vs member-assigned services
A member-specific off day blocks the public picker for that member's slots only. A SHARED service still offers slots that day, computed from OTHER team members' availability (the team union).
An 'All members' off day blocks the public picker for that day entirely. Every service (shared or member-assigned) shows the day as unavailable.
A member-assigned service whose owner is off shows the day as unavailable for THAT service regardless of whether other members are working. A prospect who really needs to book that exact service on that exact day sees a 'this service is not available on the chosen date, try another date or another service' message.
Note: Plan a company-wide vacation week (everyone is off) as ONE 'All members' off day, not as N per-member off days. The all-members entry is one row in the database and one card on the page; the alternative is N rows that are clumsy to manage. Conversely, a member's individual vacation is a per-member off day; do not block the whole team if only one person is away.
Discounts: overview
Discounts are promo codes you create for marketing campaigns, partnership referrals, or loyalty perks. A prospect types the code on the public booking form, the platform applies the discount before the Stripe Checkout step, and the booking confirms with the reduced price. Discounts are tenant-scoped (your codes work only on your booking page) and service-scoped (you can restrict a code to specific services or leave it all-service). The full configuration lives at Bookings sidebar -> Discounts.
Where the Discounts page lives
Bookings sidebar -> Discounts. The page title reads 'Discounts' with the subtitle 'Create promo codes your clients can enter on the booking form'. A maroon '+ New discount code' button sits in the top-right. Below the header, every existing discount renders as a card with its code (uppercase + monospace font), the discount type and value, the applies-to summary, the validity window, the usage counter, and three management buttons (Pause / Resume, Edit, Delete).
Discounts: the Add form, field by field
Click '+ New discount code'. A side panel opens with the discount form. Here is every field.
Code (required)
The string the prospect types on the booking form. Uppercase letters, digits, and hyphens only (the form filters input as you type). Length 3 to 40 characters. Examples: SUMMER2026, BACK-TO-SCHOOL, LAUNCH50, RIVERGATE-PARTNER-001. The code is case-insensitive at validation time (prospects can type summer2026 or SUMMER2026 and either works); the stored canonical form is uppercase. Codes must be unique within your tenant (the form rejects a duplicate with an inline 'a code with this name already exists' error); a separate tenant can use the same code for their own purposes without conflict.
Description (optional)
A short internal label that helps you (and any teammate on Team Mode) remember what the code is for. Examples: 'Back-to-school promo', 'ICN Club partnership Q4 2026', 'Returning client gratitude code'. The description appears on the dashboard discount card; prospects never see it. Treat it as internal documentation.
Discount type and Value
Percentage off: the value is a percentage 1 through 100. The platform reduces the service price by that percentage at booking time. Example: 'Percentage off' + '10' on a $50 service produces a $5 discount, final price $45.
Fixed amount off: the value is a dollar amount. The platform reduces the service price by that dollar amount, never below zero. Example: 'Fixed amount off' + '15' on a $50 service produces a $15 discount, final price $35; on a $10 service produces a $10 discount (capped), final price $0.
Both types interact correctly with multi-attendee bookings: the discount applies to the per-booking price, not per-attendee. A 100% discount on a paid service produces a free booking (and skips the Stripe Checkout step entirely; the booking confirms directly).
Applies to (service picker)
A checkbox list of every service in your dashboard. Leave ALL UNCHECKED to apply the discount to every service (including any service you add later). Check specific services to restrict the discount to those services only. Inactive services are skipped for new bookings regardless of whether they are checked. The form surfaces an info note: 'Leave all unchecked to apply to every service. Inactive services are skipped for new bookings.'
Validity window
Two date fields: Valid from (the first date the code is accepted; defaults to today on a new code) and Valid until (the last date the code is accepted; can be blank for 'no expiry'). The platform validates at the BOOKING-START time, not the time the prospect types the code: a code valid until 2026-12-31 23:59:59 in your tenant timezone is accepted for any booking that starts on or before that moment, even if the prospect's booking action happens at 23:30 and the booking start time is one second after midnight.
Maximum redemptions (optional)
An integer cap on the total number of times the code can be successfully used across all prospects. Useful for a 'first 50 customers' campaign: set the cap to 50, the platform refuses the 51st use. Leave blank for unlimited redemptions. The dashboard card shows '23 of 50 used' so you can see the campaign's pace at a glance.
Note: Discounts are validated server-side; a malicious prospect who hand-rolls a fake code or types one that has hit its cap gets a clean error on the booking form, not a stealth discount. The platform also records every successful redemption on a discount-uses ledger so you can later audit which prospects used which codes; you find the redemption history on the discount card's detail panel.
Discounts: Pause, Edit, and Delete
Pause / Resume
Pause temporarily disables the code without deleting it. A paused code returns the same 'this code is not currently valid' error a prospect would see on an expired code, but you can Resume at any time and the code starts working again with its original validity window and remaining redemption count intact. Useful for taking a campaign offline overnight while you regroup, then resuming the next morning.
Edit
Opens the same Add form pre-filled with the current values. Most fields are editable: description, applies-to picker, validity window, maximum redemptions, paused state. The code itself (the string the prospect types) and the discount type + value are NOT editable on an existing code; if you need to change those, delete the code and create a new one. This restriction protects the audit trail: a code that has been redeemed at 10% off should not later become a 20% code on the same name.
Delete
Hard-deletes the discount row. Allowed only when the code has ZERO successful redemptions; the button is greyed out (with a tooltip) on any code that has been used. For a code with redemptions, use Pause if you want to stop accepting it; the audit trail of past redemptions stays intact. Deleting a code does NOT refund or reverse past bookings that used it; the past discount was applied at booking time and stays.
The public booking page, step by step
When a prospect lands on /book/<your-slug>, they walk through a multi-step form. The platform tracks state in the URL so a prospect who refreshes the page does not lose their progress, and an abandoned-cart re-visit can pick up where they left off. Here is the canonical flow.
Step 1: Service picker
The landing page shows every ACTIVE service in your dashboard, each as a card with name, short description, duration, price, meeting mode pill, and language chips. The Popular service (if you have one) has a maroon ribbon and renders first. The prospect clicks one card to select that service. On Premium tenants with multi-language enabled, a language picker at the top of the page lets the prospect switch the page chrome between English, Quebec French, and the eight Phase 10C runtime-translated languages; the service cards translate dynamically.
Step 2: Date picker
A calendar grid showing the current month with available dates highlighted and unavailable dates greyed out. Dates with zero slots after the platform applies your availability + freebusy + off days math are unavailable; dates with at least one slot are available. The prospect can scroll forward to future months (the platform pre-computes availability for the next 60 days; further out is fetched on demand). Clicking an available date advances to Step 3.
Step 3: Time slot picker
A list of available time slots for the picked date, in 15-minute increments within your availability ranges. Slots are shown in the prospect's local timezone (auto-detected from the browser) with a small inline indicator if your tenant timezone differs (so a prospect in Vancouver booking with a Toronto tenant sees both '9:00 AM your time' and '12:00 PM tenant time'). The prospect clicks a time slot to advance to Step 4. A 'Pick a different date' link sends them back to Step 2.
Step 4: Contact form
A short form: name, email, phone (optional in v1, required for paid services since Stripe wants it), an optional matter description (a free-text 'what is this consultation about?' field), and on multi-attendee services the additional attendee fields. If the service requires a CICC consultation agreement, the rendered agreement appears below the form with the read-and-sign block. If you have a discount code, an 'I have a discount code' link reveals the code input; type the code, click Apply, and the platform either accepts it (with a 'Discount applied: -CA$5.00' inline confirmation) or rejects with a clear error. On Premium tenants with the AI document summary unlocked, an optional document-upload widget appears (drop a PDF / image of an IRCC letter, the platform runs Gemini extraction and surfaces the summary to you on the booking row for your prep). Click 'Continue' to advance to Step 5.
Step 5: Payment (paid services only)
If the final price (after any discount) is greater than zero, the platform redirects the prospect to Stripe Checkout: a Stripe-hosted page that collects card or bank info, runs 3D Secure when needed, and on success redirects back to your booking page's confirmation step. If the final price is zero (a free service, or a 100% discount), this step is SKIPPED entirely; the booking confirms directly.
Step 6: Confirmation
A success page showing the booking reference (DEM-YYYY-NNNN style or BK-YYYY-NNNN depending on tenant), the service name, the confirmed date and time in both timezones, the meeting mode (with the Google Meet or Teams link inline if online), the prospect's contact details for review, and a 'check your inbox for the confirmation email' note. The success page is the redirect target after Stripe Checkout completes (Stripe success_url) AND after the free-booking direct confirm; the prospect always lands on it as the final step.
Payment, platform fee, and refunds
When the booking enters Step 5 (Payment), the platform creates a Stripe Checkout Session via Stripe Connect on YOUR connected Stripe account. The session carries the service price as the line item, your tenant's configured currency, an application_fee_amount equal to the platform's commission (per the tiered rate), and the success_url + cancel_url pointing back to your booking page. The prospect sees a Stripe-hosted checkout flow (card input, 3D Secure if needed, optional save-card-for-future-use checkbox); when they complete payment, Stripe sends a checkout.session.completed webhook to /api/webhooks/stripe with the booking metadata, and our handler confirms the booking + sends the confirmation email + creates the calendar event.
Abandoned Checkout
If the prospect closes the Stripe page without completing payment, no booking is created and your calendar is not touched. The slot they had on hold is released back into the public picker after a 15-minute grace window (during which the booking row exists at status='pending_payment' and the slot stays reserved). If the prospect comes back to your booking page within 15 minutes and clicks the same time slot again, they pick up where they left off; after 15 minutes the booking row is purged by a cleanup cron and the slot is freely available again.
Platform fee mechanics (Basic vs Premium)
Basic tier: 1% platform fee on the first CA$5,000 of monthly Stripe-processed volume, 0% above that cap. The cap resets monthly on the first of the month in your tenant timezone. The fee is taken via Stripe's application_fee_amount at the time of capture; you see it as a separate line on your Stripe dashboard.
Premium tier: 0% platform fee on every transaction, every month, no cap. Premium pays a flat monthly subscription instead.
Per-tenant override: an Investatech-granted reduced rate for partnership or pilot tenants exists in the schema (companies.platform_fee_pct_override). If applicable to your account, you would see your specific rate on the Settings -> Billing page; otherwise the standard tier rate applies.
The fee is NEVER taken from the prospect; the prospect pays the service price. The fee is taken from YOU (the tenant) on the way to your bank account. Stripe's payout shows you the gross amount, the application fee deduction, and the net amount you receive.
Refunds
If you cancel a paid booking and refund the prospect, the platform issues a refund through Stripe Connect. The refund includes the full paid amount; the platform fee that Stripe took at capture is ALSO refunded proportionally (Stripe Connect handles this automatically when you refund the full amount, or proportionally on a partial refund). The refund flow is initiated from the booking detail panel's 'Cancel and refund' action. Refunds take 5 to 10 business days to appear on the prospect's statement depending on their card issuer.
Confirmation emails and calendar invites
The moment a booking confirms (after Stripe success for paid services, or directly for free services), the platform fires two distinct flows: an email to the prospect, and a calendar event on your connected calendar (which itself fires a separate calendar invite email to the prospect from your calendar provider).
Platform confirmation email (sent from us)
From: '<Your company name> via RCIC App' on the RCIC-App branded transporter (noreply@rcicapp.ca). Reply-To: your tenant's primary email (so the prospect's reply lands in your inbox, not Investatech's). Subject: 'Booking confirmed: <service name> on <date> at <time>'. Body: the service name, the confirmed date and time in the prospect's timezone, the meeting mode + location (or video link), the prospect's reference number, a 'see you then' framing, and (for services with a CICC agreement) the signed agreement as a PDF attachment. The email lands within seconds of the booking confirm; if SMTP soft-fails, the platform retries up to three times across a 2.5-second backoff window.
Calendar event (created on YOUR calendar)
The platform calls your connected calendar provider's API (Google Calendar events.insert, or Microsoft Graph /me/events POST) to create an event on your primary calendar. The event title reads '<service name> with <prospect name>'. The start and end times match the booking. The attendees list includes you (the organizer) and the prospect (as an attendee with response status 'needsAction' so they get the invite in their inbox). For online services on Google Calendar, the create call includes conferenceData.createRequest so Google auto-generates a Google Meet link and attaches it to the event; the link is then read back from the event's hangoutLink field and stored on our booking row + included in the platform confirmation email. For Microsoft 365 with Teams Business, the equivalent isOnlineMeeting=true creates a Teams link.
Calendar invite email (sent by Google or Microsoft)
Google Calendar and Microsoft 365 both auto-send a calendar invite to every attendee on event creation. This is a SEPARATE email from our platform confirmation; it comes from your calendar provider's no-reply address, carries the standard ICS attachment, and lets the prospect click 'Add to my calendar' to one-click-add the event to their own Google / Outlook / Apple Calendar. Most prospects use this for their reminder. The platform-side confirmation email and the calendar-side invite email are complementary; they arrive seconds apart and serve different purposes (our email is a branded record of the booking with the signed agreement attached; the calendar invite is the meeting record that lives on the prospect's calendar).
Automatic 24-hour reminders
A cron runs daily at noon UTC and identifies every booking starting in the next 24 hours. For each, the platform sends a reminder email to the prospect (same brand, subject 'Reminder: <service name> tomorrow at <time>') including the meeting mode, the video link if online, and the prospect's reference. The reminder is a courtesy nudge; calendar providers also fire their own per-attendee reminders based on each attendee's calendar default (Google's default is 10 minutes before for events; the prospect can change this on their side).
Note: If a booking's calendar event creation FAILS (a temporary Google outage, an OAuth refresh issue, a Microsoft API hiccup), the booking is still confirmed and the prospect still gets the platform confirmation email; the calendar event simply does not exist on your calendar that day. The platform surfaces a soft-warning banner on the booking row in your dashboard ('Calendar event failed; click to retry'). Click retry once your calendar connection is healthy and the platform re-creates the event.
Bookings dashboard: the list page
Click Bookings in the sidebar to land on /dashboard/bookings. The page title reads 'Bookings' with the subtitle 'View and manage your appointments'. A maroon '+ Add booking' button sits in the top-right (the manual add flow described below). Below the header, a search box and a horizontal status-filter strip let you slice the list: All / Confirmed / Pending Payment / Completed / Cancelled / No Show. Below those, the bookings list itself, sorted by start time descending (most recent first) by default.
The search box
Type to search across the prospect's name, email, phone number, or the booking reference (the DEM-YYYY-NNNN or BK-YYYY-NNNN identifier). The search runs server-side via PostgREST .or() across the relevant columns, debounced at 300 ms so typing fast does not pound the server. The list narrows live; the count badge updates. Useful for finding 'that consultation I had last September with the prospect whose name started with K'; the search reaches beyond the most-recent-100 window the list initially renders. Backslash and other special characters are stripped server-side to prevent PostgREST query injection.
The status filter strip
All (the default, with a maroon highlight): every booking regardless of status.
Confirmed: bookings that have confirmed (payment captured for paid, direct confirm for free) and the prospect is expected.
Pending Payment: bookings where the prospect entered Stripe Checkout but did not complete payment yet. These rows exist for the 15-minute grace window then get purged by the cleanup cron.
Completed: bookings whose start time has passed and you have marked the prospect as attended (either manually via the booking detail panel, or automatically if you have left auto-completion on at Settings -> Bookings -> Auto-complete past bookings).
Cancelled: bookings cancelled by you or by the prospect via the cancel link in their confirmation email. Refunded paid bookings stay in Cancelled with a 'refund issued' badge.
No Show: bookings you have marked as no-show (the prospect did not attend the scheduled meeting). Useful for tracking your no-show rate and for reaching out to no-show prospects with a follow-up.
Booking row: anatomy
Every booking renders as a card in the list. The card has a one-paragraph headline area on the left and an action button on the right. Here is everything a single row carries.
Top line: reference + status pill + service pills
The booking reference (DEM-YYYY-NNNN for demo-engine bookings, BK-YYYY-NNNN for regular booking-engine rows) renders in monospace charcoal text in the top-left. Next to it, the status pill (green Confirmed, amber Pending Payment, charcoal Completed, charcoal Cancelled, charcoal No Show). Next to the status, service-tag pills: 'Free' or '$50.00' (the price), 'Online' or 'In-person' or 'Both' (the meeting mode), and where applicable 'CICC*Agreement' (if a CICC consultation agreement was signed for this booking).
Headline: prospect name
The prospect's name rendered as a maroon H3-style heading. On multi-attendee bookings, this is the primary contact's name; the other attendees are accessible from the booking detail panel.
Meta line: service + date + time
Below the headline, a single line: '<service name> on <Day-of-week, Month Day, Year, time>'. Example: '30 min consultation on Fri, Jun 12, 1:30 p.m.'. The time is in your TENANT timezone (not the prospect's), so you always see times relative to your own clock.
Contact line: email + phone
The prospect's email and phone number on a single line, separated by a middle-dot. Both are click-to-copy on hover; the email also opens a mailto: link on click, the phone opens a tel: link.
Action links inline
View signed agreement (maroon link with an external-link icon): renders only when the booking has a signed CICC consultation agreement. Click to open the PDF in a new tab. The PDF is decrypted server-side from the agreement-pdfs bucket on demand.
Meeting link (maroon link with a video-camera icon): renders only when the booking is Online and the calendar event was created successfully (so we have the Google Meet or Teams URL). Click to open the video meeting in a new tab. The same URL is on the prospect's confirmation email and calendar invite.
Refund issued (charcoal label with a checkmark): renders on Cancelled paid bookings where the refund completed successfully. Hover for the refund amount and date.
Right-side button: Edit meeting notes (or Add)
On the right edge of every card, a single action button. The label reads 'Add meeting notes' when no Meeting Notes record exists for this booking yet, or 'Edit meeting notes' when a record exists. Clicking lands you on /dashboard/bookings/<bookingId>/notes, the eight-card Meeting Notes editor (see the Booking Notes chapter for full detail). The button label flips based on the existence of a notes record, not on whether the record has content; clicking even on a brand-new Edit-labelled booking takes you to the editor with the row already materialized.
Booking statuses, in detail
A booking's status governs what you can do with it and what the prospect sees. The status field is part of the booking row and changes over time as the booking progresses. Here is every status and the canonical transitions.
Pending Payment (transient)
The very first status when a paid booking is created. The prospect has clicked through the booking form and entered Stripe Checkout but has not completed payment yet. The booking row exists in the dashboard with this status for the duration of the 15-minute grace window. Two outcomes: the prospect completes payment within 15 minutes (status flips to Confirmed via the Stripe webhook), or 15 minutes pass with no payment (the cleanup cron purges the row and the slot is freed). Free bookings SKIP this status; they go directly to Confirmed.
Confirmed (the default working state)
The booking is locked in. Payment has been captured (paid bookings) or no payment was needed (free bookings). The calendar event is on your calendar. The platform confirmation email has gone out. You and the prospect are both expected at the scheduled time. From Confirmed, the next transition is usually Completed (after the meeting time has passed and you mark attended) or Cancelled (if you or the prospect cancel). The 24-hour reminder cron fires for confirmed bookings only.
Completed (the meeting happened)
The meeting time has passed and you have marked the prospect as attended. You can mark Completed manually from the booking detail panel's 'Mark attended' action. If you have Settings -> Bookings -> 'Auto-mark past bookings as Completed' turned on (a per-tenant default toggle), the platform auto-flips bookings to Completed when their end time passes WITHOUT explicit action from you; useful for high-volume tenants who do not want to babysit the dashboard. Completed bookings are where Meeting Notes typically get written; the dashboard surfaces a subtle 'Add meeting notes' prompt on every recently-Completed booking that does not yet have notes.
Cancelled (by you or by the prospect)
The booking is cancelled. Two paths lead here: you cancel from the booking detail panel's 'Cancel booking' action (with optional refund for paid bookings), or the prospect clicks the cancel link in their confirmation email (the link is a one-shot token; using it auto-cancels and refunds per your refund policy). The calendar event is deleted from your calendar (and from the prospect's via the calendar provider's cancellation notification). The platform sends a cancellation email to the prospect; the email includes the refund amount and the expected timing if a refund was issued.
No Show (the prospect did not attend)
You waited at the meeting time and the prospect never showed. Mark this from the booking detail panel's 'Mark no-show' action (a sibling to 'Mark attended'). No-show bookings keep their payment (no auto-refund; a no-show is the prospect's responsibility under most engagement terms), and the calendar event stays in your calendar as historical record. Useful for tracking your no-show rate over time (Bookings -> filter by No Show; the count is your KPI). You can follow up with no-show prospects manually if you want to give them a courtesy chance to reschedule.
Booking detail panel and actions
Click anywhere on a booking card (outside the right-side Meeting Notes button) to open the booking detail panel. The panel slides in from the right and shows full information: every contact field, every attendee on multi-attendee bookings, the booking timeline (created at, confirmed at, paid at, cancelled at, completed at, no-show marked at), and a horizontal action bar at the bottom.
Action bar buttons
Reschedule: opens a date + time picker pre-filtered to your availability. Pick a new slot, click Confirm; the platform updates the booking row, updates the calendar event (and re-fires the calendar invite to the prospect), and sends a 'Your booking has been rescheduled' email to the prospect with the new time. Available on Confirmed bookings only.
Cancel booking: opens a confirmation modal with the prospect's name + scheduled time. For paid bookings, a checkbox 'Also refund <amount>' is pre-ticked (you can untick to cancel without refunding, for example when the prospect cancelled with less than 24 hours notice and your terms allow you to keep the fee). On confirm, the booking flips to Cancelled, the calendar event is deleted, the refund is issued via Stripe Connect if the refund checkbox was ticked, and the cancellation email goes out. Available on Confirmed and Pending Payment bookings.
Mark attended: flips the booking from Confirmed to Completed. Use after the meeting has happened. No email is sent on this action.
Mark no-show: flips the booking from Confirmed to No Show. Use after the meeting time has passed and the prospect did not attend. No email is sent on this action.
Resend confirmation: re-sends the platform confirmation email to the prospect. Useful when the prospect says they did not receive it. Does NOT re-trigger the calendar invite; for that, you can open the calendar event on your calendar provider and manually re-send the invite.
Edit meeting notes / Add meeting notes: same button as on the list page, takes you to /dashboard/bookings/<id>/notes.
Reschedule edge cases
When you reschedule a paid booking, the existing payment stays on the booking; no new charge is made and no refund is issued. The new time slot must be available per your weekly availability + freebusy + off days (the same constraints the public booking page uses). Rescheduling does not change the booking reference; the prospect's email still cites the same DEM-YYYY-NNNN. The audit ledger records a rescheduled event so you can later see the time changed and when. Rescheduling a booking that has an attached signed CICC consultation agreement preserves the agreement; the agreement still names the original signing timestamp, not the new meeting time (the agreement is about the consultation as a whole, not about a specific time slot).
Manual: Add a booking from the dashboard
Sometimes a booking does not come through the public form. A prospect calls you, you agree on a time over the phone, and you want to land the booking on your dashboard + calendar without making the prospect go through the public form themselves. The '+ Add booking' button in the top-right of the Bookings page opens a manual booking form for exactly this case.
The manual add form
Pick the service from a dropdown of all your active services. The duration + price + meeting mode auto-fill from the service.
Pick the date from a calendar picker. The calendar greys out days with no availability per your usual rules.
Pick the time slot from a list of available slots on the picked date. The list excludes slots that conflict with existing bookings + freebusy + off days. If you NEED to overbook a slot (a recurring client who really wants the same time you already have), tick the 'Allow conflict' checkbox below the time-slot picker; the form then shows every 15-minute increment within your weekly availability ranges including conflicted ones, with a clear warning chip on the conflicted slot.
Enter the prospect's contact details: name, email, phone (optional for free, required for paid). For multi-attendee services, an attendee count + per-attendee fields.
Payment toggle: for paid services, pick 'Mark as paid (manual)' if the prospect paid you by e-Transfer / cash / cheque already, OR pick 'Send Stripe checkout link' to email the prospect a one-shot Stripe Checkout link they click to pay. The manual-paid option creates the booking at Confirmed with no Stripe transaction recorded; useful for offline payment matters. The Stripe-link option creates the booking at Pending Payment until the prospect pays.
Optional: 'Skip the consultation agreement' checkbox for services that have CICC*Agreement on. Tick this when the prospect has already signed the equivalent agreement out-of-band (a paper form they brought to the office, a previously-signed agreement on file). The booking confirms without the auto-generated agreement.
Optional: 'Skip the calendar event' checkbox. Tick this when you do NOT want a calendar event created on your calendar for this booking (a back-dated entry, a tracking-only row). The booking confirms but no calendar event is created and no calendar invite goes to the prospect.
Click Save. The booking lands on the list at the chosen time and the prospect (if you did not skip the calendar event) receives the calendar invite + the platform confirmation email.
Note: The manual add flow is useful for back-dating bookings too (the date picker lets you pick any date, not just future ones). A back-dated booking lands as Confirmed; if the back-date is in the past, the auto-completion cron (if enabled) flips it to Completed on the next run. Useful for migrating existing client history into the platform when you onboard.
Meeting Notes: the link from Bookings
Meeting Notes is a dedicated module that captures structured notes per booking. The booking row's right-side button is the canonical entry point. This section covers the Bookings-side mechanics; the full Meeting Notes feature has its own dedicated chapter (see Booking Notes in the Modules section of the manual).
When to start taking Meeting Notes
The dashboard makes Meeting Notes a one-click action from any booking row. You can start before the meeting (capture your prep notes ahead of time, paste documents the prospect sent you, write a strategy outline) or after (capture what was discussed, what the prospect committed to bring next time, what next steps you agreed on). Many tenants do both: open Meeting Notes a few hours before the meeting and structure their thinking; then during the meeting, fill in the rest as the conversation unfolds; then after, finalize and use the Make-a-proposal or Send-the-intake-form hand-offs to convert the consultation into the next concrete artifact.
The eight cards inside Meeting Notes
The Meeting Notes editor opens at /dashboard/bookings/<id>/notes with eight cards arranged vertically: Prospect (name, contact, language, role), Family Members (anyone else relevant to the matter), Matter (the immigration matter type + jurisdiction + complexity), Professional Fees (your quoted fee for the engagement that follows), RCIC Advice (your free-text strategic thoughts), Documents (file uploads encrypted at rest), and two hand-off cards (Send the Intake Form for triggering a structured intake-form invitation to the prospect, Make a proposal for converting the notes into a draft Service Proposal). On Premium tenants, a ninth card carries the AI Populate action (drop documents, let Claude extract structured fields into the notes for you to review).
Booking-time AI document summary (Premium)
On Premium tenants, the public booking form (Step 4: Contact form) carries an optional document-upload widget under the matter-description field. A prospect can drop a PDF or image (an IRCC refusal letter, a Procedural Fairness Letter, a passport scan) of up to 4 MB. The platform passes the document to Gemini 2.5 Flash with an extraction prompt, gets back a structured summary (issue type, matter category, relevant deadlines, suggested talking points), and stores the summary on the booking row.
How you see the AI summary
On the booking detail panel (the side panel that opens when you click a row), an 'AI summary' card surfaces if the prospect uploaded a document. The summary renders as structured fields plus a free-text 'observations' paragraph. You read it before the meeting starts, you know exactly what the prospect is bringing to the table, and you can prep your strategy without scrambling. The actual uploaded document is also stored (encrypted under your tenant DEK) and downloadable from the same panel; you do not have to ask the prospect to send it again.
Quota and tier gating
On Basic tenants, the upload widget is HIDDEN; prospects do not see it on the public form and no document is collected at booking time. On Premium tenants, the widget is visible but each summary call counts toward your daily AI quota (50 calls per day on Premium, shared with the AI Assistant, the SA AI Populate / AI Review, the Transfer Room message AI assist, the AFR intake AI extractor, and the Matter Work Ledger code suggester). If your quota is exhausted at the moment a prospect uploads, the document is still stored encrypted but the AI summary is skipped; you can still read the document yourself by downloading it.
The CICC consultation agreement, end to end
This section pulls together the CICC consultation agreement mechanics across the Services configuration, the public booking flow, and the post-confirmation lifecycle. Treat the auto-generated CICC agreement as a legal instrument with the same care you would apply to any signed document; the platform handles the generation + signing + storage, but the legal weight of what gets signed is yours.
Configuration prerequisites
At least one team member has completed the Phase 33 RCIC attestation. My Profile -> Regulated Consultant card. Upload a screenshot of your public CICC register page (which proves your registration is in good standing) and tick the attestation box. Until this is done, the CICC*Agreement toggle on the Service form is greyed out.
On a per-Service basis, turn on the CICC*Agreement toggle on the Add Service form for any service where you want the agreement to render. Free initial intake calls typically have the agreement off (the conversation is exploratory); paid consultations typically have it on (the consultation involves you giving information that the prospect could rely on, which Code section 24 frames as a regulated act).
Each RCIC member has saved their electronic signature once on Settings -> Signing and Signatures. The signature is captured as a typed name + chosen cursive font (or drawn on touch screen, or uploaded as a PNG). The platform encrypts the saved signature image under your tenant DEK and applies it automatically when the agreement renders.
On the public booking form
After the prospect has picked a service with CICC*Agreement on, picked a date and time, and entered their contact details, the platform renders the full consultation agreement text inline under the contact form. The text is generated in real time from your tenant settings (the RCIC's name and CICC number, the prospect's name and contact, the matter category they indicated, the service price, your tenant's office address). The prospect reads the entire agreement (a scrollable block; the platform does NOT pre-scroll to the bottom; the prospect should genuinely read it). Below the agreement: a checkbox 'I have read and understood the agreement above', a text field 'Sign by typing your full legal name', and an info note explaining that typing a name constitutes an electronic signature under PIPEDA + the eSignature law of your tenant's province.
What happens at signature
The prospect types their full name. The platform validates the name is not blank and is at least 3 characters; the form refuses Submit otherwise.
The prospect clicks Continue (to advance to Stripe Checkout for paid services, or to Confirmation directly for free).
Server-side, the platform renders the agreement HTML one more time (this time as the authoritative copy), applies the RCIC's saved signature image to the signature block on the licensee side, applies the prospect's typed name as their signature, generates a PDF via @cantoo/pdf-lib, encrypts the PDF under your tenant DEK, and stores it in the agreement-pdfs Supabase storage bucket at agreement-pdfs/<companyId>/<bookingId>/consultation-<ts>.pdf.enc.
The platform attaches the unencrypted in-memory PDF to the prospect's confirmation email (the email transport sees plaintext PDF bytes; only the storage copy is encrypted at rest).
The booking row stamps a signed_consultation_agreement_path field pointing at the storage path; the dashboard booking row shows the 'View signed agreement' link.
An audit-ledger row records the signing event: timestamp, the prospect's IP address (truncated to /24 for proportionality), the RCIC member id, and the agreement hash. The audit row is INSERT-only.
Important: Code section 24 considers a signed consultation agreement to be the moment the professional relationship begins. From that moment, you owe the prospect the duties of a regulated person: confidentiality, competence, no conflict of interest, no contingency fee, etc. The platform does not gate any of this; you do. Treat the moment the prospect clicks the consultation-confirm button as the start of the relationship.
Owner vs Staff: how bookings route in Team Mode
On a solo tenant (the default), every booking lands on you regardless of which service it was. On a Team Mode tenant, routing depends on the service's Assigned member field plus the prospect's URL choice.
Shared services (Assigned member is unset)
The public booking page picker unions every team member's availability for the picked date.
When the prospect picks a slot, the platform routes the booking to the FIRST available team member (deterministic round-robin: the platform looks at every member's load over the recent past and picks the member with the lowest count, ties broken by member id order).
The booking_row.owner_user_id field is set to the routed member's user id. The calendar event lands on that member's calendar. The 24-hour reminder cron and any other lifecycle events route based on owner_user_id, not on the tenant Owner.
Member-assigned services
The public booking page picker shows ONLY that one member's availability for the service.
Every booking lands on that member. The booking_row.owner_user_id is set at creation time; there is no per-booking re-routing.
If that member is on vacation (an off day on their slot), the picker shows the date as unavailable. The shared-services workaround does not apply because the service is locked to one member.
Per-member URLs
Each team member has a per-member URL at /book/<tenant-slug>/<member-slug>. Visiting this URL constrains the service picker + availability picker to that one member's offering only. Useful for emailing a specific prospect 'book directly with John Doe at this URL'; the prospect lands on a member-scoped page that does not invite confusion across the team. The member slug is auto-generated (john-doe for John Doe); if two members have name collisions, the platform appends a digit (john-doe-2 for the second John Doe; rare in practice).
Calendar routing on Team Mode
The calendar event for a routed booking lands on the OWNER MEMBER's calendar, not on the company Owner's (unless those are the same person). If the routed member has no calendar connected, the calendar event creation falls back to the Owner member's calendar as a safety net (so the event still gets created somewhere); the booking row's owner_user_id stays as the routed member, but the calendar event is on the Owner's calendar. The 24-hour reminder cron and the lifecycle email handling continue to use owner_user_id; the calendar fallback is purely for the calendar event itself.
Troubleshooting: Stripe disconnects, calendar lost, slots not showing
Stripe is showing as disconnected
Check Settings -> Payments. If the card reads 'Not connected', the OAuth tokens have been lost (rare; happens after a Stripe account-side security event or a long period of inactivity). Click 'Connect Stripe' to re-run the OAuth flow with the SAME Stripe account; you sign in with the existing account credentials and the platform re-establishes the connection without losing any historical bookings or payment data. If the card reads 'Connected' but new bookings are still routing prospects to a generic Stripe error page, the issue is on Stripe's side; visit your Stripe dashboard at dashboard.stripe.com for any open verification requests or account holds.
Calendar connection is showing 'Reconnect required'
Open My Profile. The Calendars card shows an amber Reconnect banner with the connected email + a 'Reconnect' button. Click Reconnect; the platform redirects you back to Google or Microsoft's OAuth flow with the same account pre-filled (you only need to click Allow again, no re-authentication usually needed); on success, the connection is restored with the same primary / secondary slot assignment. Bookings made WHILE the connection was stale still went through (the platform degrades gracefully) but their calendar events may not have created; click the soft-warning banner on each affected booking row to retry the calendar event creation.
Prospects say they cannot see any available slots
Open your own booking page in an incognito window (Cmd-Shift-N on Chrome, Ctrl-Shift-N on Edge). See what the prospect sees.
If the date picker shows zero available days for the next 30 days: check Bookings -> Availability. You may have zero time ranges configured. Or every day this week may be blocked by an Off Day. Or every slot may be consumed by existing bookings + freebusy. Spot-check by manually counting: take Tuesday's availability ranges, subtract Tuesday's confirmed bookings, subtract Tuesday's calendar busy intervals; if the remainder is empty, the picker is right to show zero slots.
If specific slots are missing on a day that should have slots: the calendar freebusy is hiding them. Open your connected calendar (Google or Outlook) for that day; look for events that are marked busy (transparency=opaque) within your availability range. Maybe you have a previously-marked busy 'Personal' event you forgot about; mark it as Free (transparency=transparent) and the slot opens back up on your booking page after the next freebusy fetch.
If ALL paid services are missing from the picker but free services show: Stripe is not connected. The platform hides paid services when Stripe is not connected (no clear payment path). Reconnect Stripe on Settings -> Payments and the paid services reappear.
A prospect wants to cancel but cannot find the cancel link
Every platform confirmation email carries a 'Need to cancel?' link at the bottom that opens a cancellation page (HMAC-signed token; the prospect does not need an account). If the prospect cannot find the email (lost in spam, deleted), you can cancel on their behalf from the booking detail panel's 'Cancel booking' action. The cancellation flow is identical regardless of who initiated; the platform sends a cancellation email + handles the refund + deletes the calendar event.
A booking shows up on the calendar but not on the dashboard (or vice versa)
Calendar event without a dashboard row: this is something you (or someone with access to your calendar) created manually on Google / Outlook outside the platform. The platform does not know about it; it does not appear on the dashboard list. The calendar freebusy still blocks the slot when the public picker runs (so prospects cannot double-book over your external event). Dashboard row without a calendar event: the calendar event creation failed at booking time (Google or Microsoft was temporarily down, or your OAuth refresh hit an issue). A soft-warning banner appears on the row; click it to retry the calendar event creation once your calendar connection is healthy.
Note: Most Bookings issues trace back to Stripe or calendar connection health. Keep both green (Settings -> Payments + My Profile -> Calendars) and the rest of the module behaves predictably. The platform never silently swallows a connection failure; you will see an amber banner somewhere that points at the issue, and clicking the banner takes you to the relevant settings page to fix it.
