Video chapters
- Custom Web and Mobile Client Development
- One organization, one booking, two clients
- Endpoint, organization and customer prerequisites
- The platform booking sequence
- Find the current API contract
- Definition, services and booking policy
- Customer sign-in and verification outcomes
- Official Flutter client and session storage
- Select the definition, service and slot
- Configured schema becomes customer controls
- Required answers before booking
- Construct the booking from selected values
- Create and read back saved answers
- Client validation and trusted enforcement
- The same booking on native iOS
- Unavailable times and unknown create outcomes
- Reschedule while preserving record identity
- Cancellation capability and owned-detail check
- Change and cancel through the mobile client
- Check the second customer boundary
- Refresh, sign out and the next visit
- Run and extend the connected client
Written guide
Written review status: revised against current source on 11 October 2026; the service-booking implementation sequence and its tested boundaries are covered below. Video status: platform-coverage revision remains in progress. The existing 10:51 review cut demonstrates customer sessions and browser/native booking, but omits dynamic definition/service selection and configured intake forms. Its technical QA passed; complete course coverage did not. The written revision below takes precedence. The approved replacement narration and revised scene renders are complete. The 22-chapter replacement has been exported and is undergoing independent audiovisual review; the older 10:51 video is not the complete revised course.
Course catalogue · Runnable source and contracts
What this course builds
A customer signs in to an organization, chooses a booking type, service and available time, completes the configured intake form, creates a reservation, returns to the same saved record on another device, then changes or cancels it. The web and native applications consume the same AppEngine customer and reservation APIs. Their design is replaceable; the identity, business hours, timezone, capacity, ownership and persisted results belong to AppEngine.
This is an AppEngine implementation course. Flutter supplies the reference UI and the official customer-authentication client. It does not replace instruction about the platform's organization, identities, definitions and API contracts.
Obtain the platform details first
Complete API Foundations: organization and addresses, 00:50. Retain your organization ID and assigned AppEngine HTTPS endpoint. Use the actual endpoint for your deployment; the public Studio address and the API base serve different purposes. The same course's customer signup, 01:51, sign-in challenge, 02:26, verification, 03:02, and refresh, 05:53 show the identity operations needed here.
For staff setup, complete API Foundations: sign in as the owner. Finish any staff verification challenge or required password change before retaining the resulting access token. Use that staff token only for configuration requests; the customer session used later is separate.
Have one organization's configuration-administrator account for service setup and two separate customer accounts for the learner and isolation check. Customer signup is not platform-organization creation and does not require a root administrator. Do not ship the configuration administrator's credentials in a browser or mobile application.
Configure the booking service
The training definition Juniper design consultations is active, has weekday opening hours 09:00–17:00, timezone America/Los_Angeles and capacity 1. It offers Room planning (30 minutes) and Whole-home consultation (60 minutes), both price 0 with a 15-minute buffer. Its Form setting points to the cc_intake_1011 collection. Keep the definition's returned sk and the selected service's exact name; they are different identifiers. Opening hours without the correct timezone do not define the same booking window.
Create the intake collection first, then the definition that references it. Both staff setup requests use POST /repository/create; their datatypes are collection and reservation_definition respectively. The downloadable intake collection defines its fields and rules. The definition setup supplies both services, hours, timezone, capacity and the Form reference. Replace names deliberately in your organization and keep the reference consistent. In Studio, open Reservations → Reservation Definitions and the chosen definition's full settings to inspect services and Form; the corresponding schema is in Database. The custom client consumes that configuration. This course uses a zero-price service to focus on connected customer clients. Paid checkout and payment-provider integration belong to the Headless Storefront course; do not present a free reservation as a demonstrated payment.
Follow setup through its saved result:
- Sign in with the staff setup identity and send the intake-collection body. Retain its
data.name; this is the Form reference. Its recordskis not the Form value. - Send the definition body with that exact name in
data.form. Keep the returned definitionskforDEFINITION_ID. - Read definitions and select that
sk. Confirm both service names/durations, workdays, hours, zone, capacity and Form reference survived saving. - Sign in as the customer and resolve the named collection schema. Confirm its
requiredarray and three properties before rendering inputs. A staff-only read is not proof that the customer client can load it. - Choose a future working day and request slots for each service. Compare their returned endpoints and spacing, then continue with Room planning.
A notification queue entry is not evidence of an email delivered. Outgoing mail is disabled in this isolated course organization. A deployed application must configure and independently test its chosen delivery provider.

Concept illustration; the following captures are actual application screens.
Register and sign in as a customer
Send orgid with requests. Customer signup uses POST /profile/customer/signup with email, password, firstName and lastName. Signin uses POST /profile/customer/signin. A response can contain a session or a verification challenge. Only a completed session permits the customer's owned reads; a challenge response is not a token.
The reference application reuses the official appmint_flutter_client authentication layer. SignedIn, NeedsVerification and SignInRejected are distinct results. The verification screen completes the official challenge object; the error screen preserves refusal. Tokens and refresh tokens never belong in screenshots, source examples or log messages.
Connect the official Flutter package
Use the official appmint-client/appmint_flutter_client package. The public AppmintAuth constructor accepts its transport and session-store interfaces. This deployment accepts ordinary customer Bearer authorization; the sample's CustomerTransport supplies that route while preserving official auth/challenge/refresh handling. It avoids the default transport's separate app-key bootstrap. No System identity is needed for these customer operations.
The browser demonstration stores tokens in memory, so reload means signin. Native uses the sample's CustomerSessionStore, implementing the official interface with OS secure storage for current and remembered sessions. The host adapter returns mutable remembered-session lists because authentication updates that list. SDK source remains unchanged. The Authentication customer-session lab demonstrates a server-held token and an HttpOnly browser cookie. This is an alternative access arrangement; it is not equivalent to browser memory.
Follow the complete local-run instructions. Public compile-time settings are endpoint, organization, definition ID and practice day. Enter customer credentials at runtime. Do not put credentials in distributable build defines.
For a separately hosted browser, permit the frontend origin at the AppEngine deployment. The current self-hosted production setting is ALLOWED_ORIGINS (comma-separated origins). A browser CORS refusal is distinct from a failed customer signin; native HTTP does not use browser CORS. The reference web build was tested against the permitted local training origin, with no local address displayed in the lesson.
Find the current API contract before building the flow
Open the maintained AppEngine API Reference. Search for reservations and open GET /crm/reservations/definitions. Read the authorization/header section, the response schema and its example. Follow the selected definition into the slots operation and then the customer reservation operation. Open the schema section for the configured collection when the definition names a form. The point is to learn where the current contract lives, then apply it; this course does not ask you to memorize a frozen JSON listing.

Captured from the current local reference implementation, without sending credentials or changing records; this is not a claim that the hosted deployment was reverified.
Useful exact reference links:
| Operation | Current reference |
|---|---|
| Discover bookable definitions | GET definitions |
| Ask for one service's times | POST slots |
| Resolve the configured collection schema | GET collections |
| Submit the customer's booking | POST customer reservations |
| Create the setup collection/definition as staff | POST repository create |
| Read a customer’s saved booking | GET reservation detail |
| Read the customer’s bookings | GET customer reservations |
| Load a range for rescheduling | POST availability |
| Move the owned reservation | POST reschedule |
| Cancel by email and booking reference | DELETE cancellation |
Reference example correction (14 October): the inspected documentation still showed older servicePointId/startDate/endDate examples for booking and slots. The demonstrated, source-verified flow uses reservationDefinitionId + serviceName + serviceDate for slots, then reservationDefinitionId + service + returned startTime/endTime + configured form answers for creation. The narrow documentation correction is applied to the local source and shown in the labelled reference preview. Hosted deployment has not been verified. Slot length and buffer come from the selected service; the request does not override them with an arbitrary duration.
The HTML reference is the navigation surface; endpoint links expose the maintained detail directly. Use the endpoint assigned to your deployment if its installed version differs from the hosted reference. A documented route is not a substitute for opening the current reference before integration.
Discover definitions and let the customer choose a service
A reservation definition is the organization's booking policy. Its sk is the identity used in subsequent requests. Its title is a display label. Its services have their own names, duration, price and optional recovery time (breakAfter). Do not use a display title as the record ID or quietly substitute the first service.
Read the definitions endpoint. The current service returns active definitions, enriched for the organization. Active discovery is not a guarantee that every returned definition can take a booking: additional bookable/type rules can still make the slots response unavailable. Do not silently convert a table, event or host-dependent definition into this timed-service UI. Render a booking-type picker from the returned records and a service picker from the chosen definition's data.services. Show duration and price before time selection. When the definition changes, discard its old service, host, slot and form answers, load the new form, then fetch new availability. When the service changes within that definition, clear the selected time and query again because its duration and recovery time can change which slots fit.

The extended practice definition Juniper design consultations offers Room planning (30 minutes,15-minute buffer) and Whole-home consultation (60 minutes,15-minute buffer). Both are free in this example. Select each and compare its returned times. A paid service needs the payment flow taught in Headless Storefront; a numeric price by itself is not proof of a completed charge. When services is absent, the current fallback service uses data.name and data.duration (or 30 minutes). An explicitly empty services array is a different configuration and must be repaired or rejected, not assumed to be the same fallback. The provided fixture has two named services. Inspect the returned contract before extending the client to other definition types.
Read availability
Read GET /crm/reservations/definitions and choose the intended definition. Call POST /crm/reservations/slots with reservationDefinitionId, serviceName and serviceDate (YYYY-MM-DD). Retain exact returned start/end instants and business timezone. Display using timezone data, not a hardcoded UTC offset or the device timezone. A visitor travelling overseas must still book the business's intended local hour.
An empty day is a valid outcome. Offer another date. The closed-day response can contain slots: [], closed: true and an explanation in error; the one-day request can succeed at HTTP level while offering no times. Do not treat that explanation as a customer-authentication failure. A slot result is availability at the moment it was read, not a held place. The create operation rechecks capacity.
Understand the choices that change availability
| Configuration | Effect on this flow |
|---|---|
officeHours.timezone, workdays and blocked times | Interpret the requested calendar day in the business's zone; show a closed day as an explicit outcome. |
Service duration and breakAfter | The meeting's end and the occupied recovery window differ. The buffer must remain unavailable even though it is not meeting time. |
| Slot-generation mode | Duration-based starts and interval-based starts are different policies. Use server-returned slots; do not reconstruct them by adding a hardcoded30 minutes. |
| Host selection / bookingType | An explicit host must belong to the definition and be available. Leaving the host unspecified lets the configured unassigned/round-robin policy apply. Changing hosts invalidates a previously selected slot. |
spots, party limits | Available places and allowed party size are distinct constraints. Read the response and preserve server refusal if another customer takes the place. |
For a range calendar, POST availability accepts reservationDefinitionId, optional serviceName, from as a business-calendar date and days (default 7, at most 31), plus excludeReservationId when moving an existing booking. The small client uses the one-day slots operation and omits its current occupied start; a range-calendar implementation should use the exclusion-aware operation. Use the existing record's ID to exclude its own occupied place, not another customer's booking. Event, service-point and pipeline definitions have additional setup; this service-client example must not silently relabel them as ordinary timed consultations.
Resolve and render the definition's configured form
The selected definition's data.form points to a collection name, not a submitted form record. The demonstrated reference is cc_intake_1011. Resolve that name through the current repository collection-schema operation and select the matching collection's data.schema. An enriched definition may instead carry a collection object; inspect its data.schema. Do not request broad staff/customer data just to render form fields, and never embed a staff token in the browser to bypass a schema-read refusal.
The collection lookup returns a list: choose the entry whose data.name matches the configured name, then use its data.schema. Do not assume the HTTP response itself is the schema or silently take a different collection’s first result.
The controlled customer session successfully read /repository/collections/cc_intake_1011. Organization enrichment can change the schema, so fetch the current schema when selecting the definition. A failed schema request, a missing named collection, or an enriched form object without a usable schema must stop the booking details step with an understandable retry/error state. The sample rejects these states instead of turning them into an empty form. An absent form means no extra intake; a configured form that failed to load is a different state.

Concept illustration with an actual native capture. Each property keeps its API key while the client renders its human title and required control. The video graphic reveals these relationships one at a time.
The practice form contains Project type, a required choice between Single room and Whole home; What would you like help with?, required text with a 12-character minimum and 300-character maximum; and I agree to be contacted about this booking, a boolean that must be true. Render human titles and descriptions, while retaining property names as the answer keys. The title may change without changing the payload key.
In the reference client, follow this sequence: select Room planning → choose its available time → show the newly resolved intake controls → submit empty → see the relevant field errors without a booking request → choose Single room, enter “Plan a reading corner with accessible shelving.” and accept the contact choice → review the selection and submit. Keep the answers visible after a network or slot conflict so the customer can choose another time without retyping them.

This native test left the required answers empty and verified that no booking POST occurred. The errors come from the fetched schema.

The captured errors above are rechecked on submission; entering a value does not immediately remove every old error in this recording. The saved-result chapter confirms the completed values.
The downloadable renderer covers text, enum, number/integer and boolean controls and the constraints used here. It is a bounded renderer, not a full JSON Schema engine: unknown schema/field rules (including composition and conditional rules), malformed properties and required names without corresponding controls stop booking. It supports required, enum, const, string length/pattern and numeric minimum/maximum; optional nonnumeric input is still an error in a numeric control. Files, nested schemas, conditional rules and remote data-source controls require their own renderers; the sample identifies unsupported controls and blocks submission instead of silently dropping them. For attachment intake, use File Storage: customer upload, list and delete and stored files versus record references and then pass the resulting field value according to the configured schema. Do not send a device-local file path or invent a second upload protocol.
Validation boundary: client validation makes the form usable; it does not secure a direct API caller. Current reservation creation visibly enforces booking rules and preserves submitted answers, but this audit has not established server enforcement of the referenced form's required/enum/length constraints. If those answers are mandatory for your application, enforce the same approved schema at your trusted application boundary. The successful readback below proves persistence, not an unperformed invalid-payload rejection test.
Construct the booking from the selected state
| Payload value | Where it comes from |
|---|---|
reservationDefinitionId | The selected definition's sk. |
service | The selected service's name; the slots request calls this value serviceName. |
startTime, endTime | Exact instants from the selected returned slot, not a newly formatted device-local string. |
form | The validated answer map keyed by configured schema property names. |
| Customer ownership | The authenticated customer session; do not trust a public owner-ID field. |
| Optional host/party choices | Only controls supported by the selected definition and current operation contract. |
For the extended example, form is { "projectType": "Single room", "brief": "Plan a reading corner with accessible shelving.", "contactConsent": true }. The form title is not an answer key. Keep these answers nested under form; do not flatten brief into the reservation root or send the schema itself as the answer. Flat booking request fields become savedRecord.data on readback, so the stored answer location is savedRecord.data.form.
Do not copy the whole definition or a previous enriched reservation into a create request. Send the deliberate booking inputs. AppEngine calculates/sets record identity, reservation reference, status, timezone, configured meeting details and other platform fields. Omitting endTime can use the service duration, but retaining the chosen slot's exact endpoints makes the demonstrated selection explicit.
Create and verify the saved booking
Send POST /client-data/reservations with the selected definition, service, slot instants and configured form answers. Notice service, not the slot request's serviceName. The customer's session supplies ownership; do not let a public form choose an arbitrary owner. The response gives sk and the customer-facing reservation reference in data.name.
Read GET /client-data/reservations/:id using the returned sk. Show the saved service, time, timezone, reference, status and data.form answers. Compare each answer to the submitted value; an optimistic local form is not the saved result. Show success after this readback. Hiding a loading indicator or showing an optimistic card alone is not proof of a saved reservation.

Actual browser result. The saved reference is read from AppEngine.
Continue on web and native
The same customer signing in to the same organization on another device reads GET /client-data/reservations. The server list and exact detail record provide continuity. No record is copied from browser storage to the phone. The native app is an actual Flutter iOS build; a narrow browser window is not evidence of mobile execution. Android uses the shared source but is not yet independently tested in this course.
The native test passed signin, availability, create, reread, another customer's exclusion, reschedule, cancellation, refresh and signout. Sanitized native outcome. The browser-created booking was then opened, rescheduled and cancelled in the actual iOS application; its complete changed and cancelled cards were inspected after recording.

Actual native app capture, not a resized browser.
Handle availability conflicts and unknown writes
If another customer takes a displayed time, the create request can return 400. Preserve the response explanation and reload availability. Do not turn a refused request into an empty booking list. For a transport interruption, the server may have saved a record even though the client never received the response. Inspect the customer's saved list before repeating a create. Do not automatically retry uncertain writes. In this client, open My bookings and compare the intended service, start time and submitted answers with the saved records. If the result is ambiguous, retain the entered values while investigating; absence from one stale screen is not proof that the write failed.
The native/browser reference shows a loading state during network work and disables duplicate actions. User choices are separate from waiting for network: the reschedule dialog must not retain an endless progress indicator while waiting for a click.
Reschedule the same record
Use POST /crm/reservations/reschedule/:id with a fresh startTime and reason. The customer token is checked against ownership. The practice request uses notify:false; it does not claim an email was delivered. Read back the same ID, verify the same reference, changed start/end and stored reschedule history. Keep the current booking visible until the change is confirmed.
A reschedule conflict currently returns 409 with reason: slot_unavailable and problems; selecting the unchanged start without a service change returns 400 with reason: same_time. A move keeps the previous duration unless a new service is explicitly selected, in which case AppEngine derives the duration from that service. The reference UI changes time only. Customer clients cannot use a staff override to ignore capacity or host constraints. Host/party controls, paid checkout and switching service while moving are extension paths discussed here; the shipped UI demonstrates unassigned, one-place, free-service time changes, not those unrun variants.
The other customer's attempt to reschedule this ID returned 404. A full or closed destination is a refusal, not a completed move. The application's Change time control fetches fresh availability before offering choices.

The reference YPBLZHAQ remains unchanged while the saved time changes from 09:00 to 09:45. The configured answers remain alongside that same record.
Cancel and retain history
The established cancellation route is DELETE /crm/reservations/cancel/:email/:reservationCode. Encode both values as path components. This is an email-and-code capability route; it does not prove bearer ownership. The client first reads the owned detail and takes email/code from that saved record. A server gateway should enforce that preflight server-side too. The preflight does not change the direct API's access model.
After cancellation, reread by ID and display cancelled. The saved record remains in history. A repeat cancellation returns the existing cancelled result. Encode the saved data.name reference, not the record sk, in the cancellation path. Deleting a card locally is not cancellation. Avoid the legacy client-data delete shortcut whose argument forwarding does not match this contract.

Verify customer separation
Sign in as a second customer in an independent session. Their list must omit the first customer's booking. Use separate customer tokens and the same organization; changing only the UI label is not a second identity. Reading its ID through the customer-owned detail route returned 404; authenticated rescheduling returned 404 as well. Missing and inaccessible IDs intentionally share this result. These tests establish those particular owned routes, not every legacy email/reference route.
Refresh, restore and sign out
The refresh request uses refresh_token at POST /profile/customer/refresh. Update stored credentials and prove the refreshed session by another profile/owned-reservation read. The official authentication callbacks provide renewal; on a refused renewal return to signin. Signout clears this client's session and is not a claim of global revocation across devices.
The official package distinguishes current signout from remembered accounts. signOut() clears the active token and cached person, but leaves remembered accounts in the session store. For a “forget this account on this device” action, call forgetSession(email, orgId) as well; that removes the matching remembered entry and does not revoke tokens on other devices. The sample does not expose a remembered-account picker, so do not imply that button exists.
Native secure storage enables restoration on a later launch. The sample partitions its store by organization, so changing organizations must select the matching session store and credentials, not reuse the previous organization’s token. Browser memory intentionally does not survive reload. Test the next visit separately from first signin when choosing your deployed session arrangement.
Equivalent requests and implementation checklist
The download includes Flutter, TypeScript, JavaScript, Python, curl, JSON and Postman examples. They preserve HTTP statuses and raw JSON. The smaller HTTP clients illustrate the reservation contract; use the full official authentication layer or linked Authentication lesson for verification/challenge completion. The source package excludes private defines and generated builds.
Before extending this client, check each outcome: the selected definition and service produce the expected times; configured questions load and validate; a saved booking contains the exact answers; the same customer reads it on another device; rescheduling retains identity and answers; cancellation remains in history; a second customer cannot read or move the owned record. Check a failed schema read and an unavailable time as well as the successful path. These are platform integration checks, not just whether a screen looks complete.
Revision evidence and remaining capture

The native configured-form journey independently saved THXZN0SP after changing services, verifying required fields blocked the POST, and filling the fetched schema. It reread all answers, then cancelled and confirmed the answers remained. production/qa/native-form-result.json records the calls and assertions. The browser version independently saved YPBLZHAQ with these answers. That exact booking was then read, moved and cancelled on native iOS, with every answer preserved; production/qa/form-cross-device-result.json records the result. The completed raw captures still require the revised narrated edit.
The configured-form API proof saved reservation CU6IWC1R, reread all three answers exactly and retained them after cancellation. Evidence is in videos/appengine-connected-client-usecase/production/lab/form-flow/. The revised client now exposes definition/service selection, schema resolution and intake validation. Native and browser controls and maintained-reference navigation have now been captured. Those raw clips and the revised explanation need to replace the old video middle; they have not yet been assembled into a new narrated export. The previous cut remains available for review, with its missing coverage clearly labelled. No new narration/export is claimed by this written revision.
Demo source code
AppEngine Web and Mobile booking client on GitHub. Runnable Flutter web/iOS booking app with the official client snapshot, configured forms, session handling, rescheduling and cancellation; JavaScript, TypeScript and Python HTTP examples included.
The downloadable ZIP is the recording-time snapshot. Use GitHub for the maintained examples. Each app has its own run instructions; no private credentials are included.