Opens an Intercom conversation for the traveler, carrying the help-app context AND server-side support fields the app never sees (partner, promocode, iccid, hubby_user_id — read from the user/eSIM). The agent/Fin skips discovery.
The traveler's help context (from /help/context) plus the stuck-step info is sent as the body; support fields are attached server-side.
One open conversation per traveler. If the traveler already has an open conversation (open in Intercom and written in within the last 7 days — the same rule as open_conversation_id on /help/context), no second one is created: the existing id comes back with reused: true, the new context is added to that conversation as an internal note, its custom attributes are refreshed, and the contact is refreshed too — its latest_esim_* attributes to the eSIM asked about now, with the durable attributes, name and email as the escalation has them.
open_conversation: false — the app opens Intercom's Messenger itself (dual mode, ADR 0056; a 3.0.1 app). The reuse rule above runs with one difference: it looks for the conversation that blocks the Messenger's composer — state open or snoozed, of any age, with no seven-day window (ADR 0051 amended 2026-10-05) — rather than for an open conversation in the sense of /help/context. Intercom refuses a new conversation beside either state, and the page opens the Messenger's home or its composer by the same rule. Otherwise, instead of creating a conversation, the backend writes the context onto the traveler's Intercom contact (help_context, help_path, entry_state, step_stuck, locale, help_context_at, beside the durable attributes; retried without the six if the workspace has not created them), keeps it, and answers { ok: true, contact_id }. When Intercom's webhook reports the conversation the traveler then starts (conversation.user.created, within 15 minutes of this call), the backend posts the kept context into it as an internal note, once. A visitor under the flag touches Intercom not at all and is answered { ok: true, contact_id: null }. Any other value, or the flag absent, is today's behaviour — an old client never sends it — with one addition: before opening the conversation itself, the backend keeps a bodiless context so the webhook recognises the conversation as already described.
A conversation the traveler starts in the Messenger on their own (its composer, a push), with no call to this endpoint behind it, is described after the fact: when the webhook reports it (conversation.user.created) and no kept context claims it, the backend resolves the traveler from the contact's external id, describes their eSIM by the Help Centre's default rule, and writes the same contact attributes (the latest_esim_* rows included), an internal note whose first line says the conversation started from the Messenger, and the conversation attributes. Never for a visitor. Best-effort, and once per conversation: the description is claimed on the conversation's own record before anything is posted, so a redelivered webhook describes nothing twice.
The context note is one fact per line, on every path that posts one. The first line, which an inbox previews, is Traveller is stuck at "<stuck_at>", plus (before the trip) or (at the destination) when the path says which; without stuck_at it falls back to the step or path id. Then Device, eSIM with its status, provider and ICCID, Type (data limited, time limited, starter, unlimited, or Universal · <type>), Destination, Data left, Days left (every type but data limited, and only for a package that is running: one waiting to start has no days left), Promo code and Partner (both from the described eSIM's own document, the partner falling back to the account's), then what the traveler corrected and typed. For a visitor (not signed in) the first line ends . Not signed in, no account details. and the note has no Partner line. A fact the backend does not hold has no line. The step and path ids are not lines; they stay as the step_stuck and help_path conversation attributes. The contact attribute promocode is the described eSIM's own promo code. The contact also carries the described eSIM as the latest_esim_* attributes support reads their reports from, under the keys and in the shapes the 2.x app wrote: latest_esim_iccid, latest_esim_status, latest_esim_imsi, latest_esim_partner (the partner's document path, companies/hubby/partners/<id>), latest_esim_country (the catalogue's name), latest_esim_provider, latest_esim_activated_at, latest_esim_time_assigned, latest_esim_promo_code, latest_esim_package_count and latest_esim_package_1..3 (<total bytes> - <status>). Written on every escalation for a signed-in traveler, a reused conversation included, and never for a visitor. Every row is always sent: a fact the described eSIM lacks, or every fact for a traveler known to have no eSIM, is cleared (null, where the 2.x app wrote the string none), so the rows describe one eSIM, and only the one the traveler has now. When the eSIM could not be read, no row is sent and the contact keeps what it had: a failed read is not "no eSIM".
The language the traveler picked (language). When sent, the conversation opens with the traveler's pick as their own first message (e.g. 🇳🇱 Nederlands) and the context goes to the agent as an internal note the traveler never sees. The contact's language_override is set to it before the conversation is created, so Intercom's routing rules can read it; it is also set as the conversation attribute support_language, in its own call. Without it — or with a value support does not work in — the context opens the conversation, as before.
A visitor (not signed in), authenticated with a visitor pass, reaches a person as an Intercom lead: no account is read, no contact attributes are sent, and no external_id (Intercom keeps none for a lead). Their lead from an earlier conversation is reused. The context note says they are not signed in. Rate limited per visitor (5 an hour → 429).
Authentication: Bearer token — either the webapp JWT from /webapp/auth/exchange, a native Firebase ID token, or a visitor pass from /help/visitor.
Requires: INTERCOM_ACCESS_TOKEN configured, and the custom attributes pre-created in Intercom (see helpChat/README.md).