Skip to content

Worked example · for developers

Connecting one app, end to end.

The integration guide is the reference — every piece, in isolation. This is the tutorial: one concrete app built start to finish, so you can see how the pieces fit, and the failure modes you'll meet on the way. The app here is a simple bookings platform — customers have a profile, book appointments with providers, and reschedule or cancel them.

7 operations4 reads · 3 writesArabic + English in one chat≈ a day of work

01 — The scenario

The scenario

Everything on our bookings platform lives behind a login: your profile, your upcoming appointments, your account balance. Before this integration, the support assistant could explain how booking works — but a customer asking "do I have anything booked this week?" got a generic answer, because the assistant had no idea who was typing.

The goal is to close exactly that gap: let the assistant read the customer's own account and act on it — book, reschedule, cancel — in the same chat, in Arabic or English, without ever exposing a real credential to the language model.

Everything here follows the pattern guide

If a term is new — handoff token, Bridge, the identity allowlist — the integration guide defines it. This page shows it in one concrete app, with real consequences.

02 — The result

What the assistant can do

Seven operations. Each answers a real thing customers ask — and they ask in Arabic, Iraqi dialect, and English, often mid-sentence.

شنو مواعيدي القادمة؟“What are my upcoming appointments?” → list_appointments → “You have one on Thursday at 5 PM with Dr. Sara.”
الغي موعدي يوم الخميس“Cancel my Thursday appointment” → cancel_appointment → “Done — your Thursday 5 PM appointment is cancelled.”
2026/07/23 i want to book on this datelist_providers then book_appointment → “What time on July 23rd, and with which provider?”

The full allowlist

// A bookings platform's allowlist — the exact surface the assistant is given.
// Reads answer "what's my…"; writes act on the customer's own account.
allow = {
  // ── reads ──
  get_profile:            { GET,   "/api/users/me" },
  list_appointments:      { GET,   "/api/appointments/me" },
  list_providers:         { GET,   "/api/providers" },
  get_wallet:             { GET,   "/api/users/me/wallet" },
  // ── writes — your own API still enforces every rule (availability, lead time, limits) ──
  book_appointment:       { POST,  "/api/appointments" },
  reschedule_appointment: { PATCH, "/api/appointments/{appointmentId}/reschedule" },
  cancel_appointment:     { POST,  "/api/appointments/{appointmentId}/cancel" },
}
Reads first, then writes

Ship the four reads and verify them before switching the writes on. A read that misbehaves is an information issue; a write that misfires books or cancels a real appointment. Your own API keeps enforcing its rules — availability, lead times, limits — so the Bridge grants reach, never permission.

03 — The build

Building it in order

Four pieces, three of them yours. Budget about a day.

  1. A mint endpoint. An authenticated route that returns a short-lived opaque token for the current user. (Where it lives matters more than it looks — see the failure modes.)
  2. An exchange endpoint. Server-to-server, shared-secret, turning that token into a real user token — and refusing to do so for admin accounts, so support can never become an escalation path.
  3. A small Bridge service. The seven-operation allowlist above, and nothing else. Path ids like {appointmentId} are filled from the request body; the user's real token never leaves the Bridge.
  4. One widget attribute + seven dashboard actions. The embed gains a data-end-user-token-url; each bridge operation becomes an action whose only auth header is {{ctx:end_user_token}}.

The integration guide has the full code for each of these — this walkthrough is about the order and the pitfalls, not re-printing it.

Build against staging

Do all of this against an isolated staging copy — its own database, its own secrets, customer data scrubbed of real emails and phone numbers. The failure modes below are much cheaper to meet there than in production.

04 — What goes wrong

Failure modes to expect

An integration can "work" on day one and still feel broken. These are the traps that bite in practice — every one is invisible until a real conversation surfaces it.

1 · The bot "forgets who you are" after an hour

The headline symptom: an action fails with the assistant insisting it doesn't know who the customer is — even though they're plainly signed in. It's reproducible, but only an hour or so into a session.

The cause is almost always the mint endpoint being authenticated by a short-lived access token. Once that lapses, the mint returns 401, the widget sends no identity, and every operation fails as anonymous while the person is still logged in.

# The failure, reproduced deterministically:
# drop the short-lived access cookie, keep the long-lived refresh cookie, mint again.

grep -v access_token jar.txt > expired.txt

curl -b expired.txt -X POST $API/api/support/handoff-token       # → 401   ✗  (mint under /api/support)
curl -b expired.txt -X POST $API/api/auth/support-handoff-token  # → 200   ✓  (mint under /api/auth)

# If your refresh cookie is path-scoped to /api/auth, it never reaches /api/support.
# Put the mint endpoint where the long-lived credential already travels.
The fix, and the lesson

Put the mint endpoint where your long-lived (refresh) credential already reaches, and validate that credential read-only — never rotate it, or you could log the user out from a support chat. This is the "credential-lifetime trap", and the reason the guide insists you test the expiry case before shipping.

2 · A wrong answer that won't go away

After identity is fixed, one exact question keeps replaying an old failure — while rewording it works perfectly. The assistant caches answers by question, not by user, for a few days. Anything asked during an outage has its failure answer frozen in, so a transient bug becomes a persistent one. Purge the tenant's answer cache; identity-bearing and action-driven answers aren't cached going forward.

3 · A blank reply

Occasionally an action succeeds and the model returns nothing — an empty bubble. A naive retry can make it worse (an identical request replays the identical empty result). The platform handles this for you now — it retries on a stronger model and only hands off to a human if that's empty too — but it's worth knowing the shape of it.

4 · It suddenly answers English in Arabic (or vice-versa)

With a bilingual knowledge base, a model can start following the language of the retrieved snippets or its own earlier turns rather than the customer's latest message. The platform now pins each reply to the language of the message it's answering — but if you see drift, it's this.

5 · The right data, the wrong number

If your API returns two fields that disagree — say a raw column and the value your system actually enforces — the model has no way to know which to trust, and will sometimes quote the wrong one. The fix is one sentence in the action's description: name the authoritative field and tell it to ignore the other.

6 · Trigger phrases that do nothing

The dashboard's "trigger phrases" field is not sent to the model. If you put your Arabic phrasings there expecting them to help the assistant choose the right operation, they won't. They belong in the description, which is the only free text the model actually reads.

05 — Takeaways

Lessons & tips, distilled

✓ Do

  • Test the expiry case first — it's the break you'll actually hit.
  • Put your phrasings in the description, not trigger phrases.
  • Ship reads before writes; trust the logs before you trust the model with a booking.
  • Name the authoritative field when your data has two that disagree.
  • Build on staging with scrubbed data.

! Traps that look like other problems

  • "The bot is dumb" is usually a stale cache or a dropped token — not the model.
  • A ~1s reply is a cache hit, not speed. Latency tells you which.
  • "It stopped doing the bad thing" ≠ the fix works — verify the mechanism, not just the symptom.
  • An expired credential looks nothing like a crash; it looks like amnesia.
The meta-lesson

Almost every failure here is invisible — no error, no crash, just a subtly wrong or generic answer. The way to find them is to hold a real conversation, in your users' language, and watch the reply and the latency. A test suite proves the code runs; only a real chat proves the assistant is helpful.

06 — Summary

At a glance

DimensionThis example
Operations exposed7 — four reads, three writes
New endpoints on your side2 (mint + exchange) plus one small Bridge service
Scode-side code0 — dashboard actions and one embed attribute
Credentials the model can see0 — only the opaque token travels, and even that is hidden from it
Languages handled in one chatArabic, Iraqi dialect, Franco-Arabic, English
Rough effortAbout a day for a developer who knows your stack

Ready to build? The integration guide has the code for every step, and its troubleshooting table is keyed on exactly the symptoms above.