INTERCOM_WEBHOOK_TOPICS is the same tuple IntercomChatbot uses in the event connector for session-link webhooks, so what you subscribe to in Intercom stays aligned with parsing and linking.
intercom_chatbot_webhook_url builds the correct /chatbot-webhook/{product_id} URL and rejects invalid product_ids; format_reactive_session_link_script matches optional POST /sessions/link when you still need browser-side linking. Proactive quick_reply helpers (correct Intercom-Version: Unstable, pingβpong trigger, optional connector LLM-label URL) are documented in Proactive Messenger quick replies below.
Module: autoplay_sdk.integrations.intercom
Small, dependency-light helpers in autoplay_sdk/integrations/intercom.py for Developer Hub URL/topics, the reactive snippet, and proactive Messenger quick replies.
Webhook topics
Subscribe in Intercom to exactly these topics so session linking and support AI agent delivery stay aligned withIntercomChatbot parsing in the connector:
| Name | Constant | Value |
|---|---|---|
| User created | INTERCOM_WEBHOOK_TOPIC_USER_CREATED | conversation.user.created |
| User replied | INTERCOM_WEBHOOK_TOPIC_USER_REPLIED | conversation.user.replied |
INTERCOM_WEBHOOK_TOPICS β both strings above. Other topics are ignored by the connector.
intercom_chatbot_webhook_url(connector_host, product_id) -> str
Builds the absolute HTTPS URL for Intercom outbound webhooks:
{origin}/chatbot-webhook/{product_id}
connector_host: hostname (event-connector-xxxx.onrender.com) or full origin (https://β¦). Trailing slashes stripped; bare host getshttps://prepended.product_id: non-empty, no/(single path segment).
ValueError if host or product id is invalid.
format_reactive_session_link_script(connector_host, product_id) -> str
Returns an HTML/JS snippet that registers Intercom("onConversationStarted", β¦) and POSTs to /sessions/link with product_id, PostHog session_id, and conversation_id.
Use only when you still need browser-side linking; prefer webhooks to /chatbot-webhook/{product_id} first.
Proactive Messenger quick replies
A proactive quick reply is an admin message on the conversation withmessage_type: quick_reply: an intro body plus up to three tappable reply_options. Send it with POST {INTERCOM_REST_API_BASE}/conversations/{conversation_id}/reply.
Split responsibilities:
| Concern | Module | What you use |
|---|---|---|
| When to show an offer | autoplay_sdk.proactive_triggers | ProactiveTriggerContext, ProactiveTriggerRegistry.evaluate_first, ProactiveTriggerResult (copy, optional labels, interaction_timeout_s, cooldown_s). |
| How to send on the wire | autoplay_sdk.integrations.intercom | intercom_quick_reply_http_headers, build_intercom_quick_reply_reply_payload β never hand-roll Intercom-Version. |
| Idle teardown (chat) | autoplay_sdk.agent_states + autoplay_sdk.integrations.intercom | run_proactive_idle_expiry with delete_remote_chat_thread using build_intercom_delete_conversation_request + DELETE; then clear_local_chat_thread_state (sessionβthread + persisted FSM β host-owned). |
Prerequisites
conversation_idβ From Intercom webhooks,SlimActionconversation_id, or afterPOST /sessions/linklinks the browser session.- Access token β Bearer token for the Intercom API (app allowed to reply as admin).
admin_idβ Workspace/agent id Intercom expects onquick_replypayloads (from Intercom app settings).
Recipe: end-to-end
-
Build context β Instantiate
ProactiveTriggerContextwith chronologicalcanonical_urls(andsession_id,conversation_id,action_count,product_idwhen you have them). -
Evaluate β
result = registry.evaluate_first(ctx)usingdefault_proactive_trigger_registry()or your ownProactiveTriggerRegistry([...]). IfresultisNone, do not send. -
Gate (recommended) β Cooldown: skip if
now - last_fired_at < result.cooldown_sfor(conversation_id, result.trigger_id)(you storelast_fired_at). FSM: with Agent session states, callcan_show_proactive_with_reason()before sending; after a successful send, move toproactive_assistance. When the user never engages pastresult.interaction_timeout_s, userun_proactive_idle_expiryfor chat surfaces (notexpire_proactive_to_thinking_if_idlealone): implementProactiveIdleExpiryHooksβdelete_remote_chat_threadperformsDELETE /conversations/{id}viabuild_intercom_delete_conversation_request, returnsTrueonly on 2xx or 404; then the orchestrator runsexpire_proactive_to_thinking_if_idleandclear_local_chat_thread_state(sessionβthread + persisted FSM β your host). See Delete conversation (idle teardown). -
Build the POST β
headers = intercom_quick_reply_http_headers(access_token).payload = build_intercom_quick_reply_reply_payload(admin_id=..., body=result.body, prompt_labels=list(result.reply_option_labels)). Emptyprompt_labelsis fine (intro-only). -
Send β
POST{INTERCOM_REST_API_BASE}/conversations/{conversation_id}/replywithjson=payloadandheaders.
quick_reply, the session FSM is proactive_assistance. If the user sends normal chat text that does not match the configured chip label, the connector moves the FSM to reactive_assistance and runs the usual RAG reply pipeline so a real question is answered. Tapping the chip (message text matches the label) still enters guidance_execution with the proactive expert-help flow id.
Delete conversation (idle teardown)
Forrun_proactive_idle_expiry β delete_remote_chat_thread, build the HTTP target from pure helpers (Bearer + Intercom-Version: 2.15 via INTERCOM_API_VERSION_DELETE_CONVERSATION):
build_intercom_delete_conversation_request(access_token, conversation_id, *, retain_metrics=True) -> tuple[str, dict[str, str]]β returns(url, headers)forclient.delete(url, headers=headers).- Lower-level:
intercom_delete_conversation_url,intercom_delete_conversation_headers.
DELETE succeeds β the SDK orchestrator encodes that order.
Example (Python)
Helpers reference (delivery)
Quick replies must useIntercom-Version: Unstable. Do not reuse a numeric Intercom-Version from other Intercom REST calls.
| Constant | Value |
|---|---|
INTERCOM_HTTP_HEADER_VERSION | "Intercom-Version" |
INTERCOM_API_VERSION_QUICK_REPLY | "Unstable" (alias: INTERCOM_API_VERSION_UNSTABLE) |
| Helper | Purpose |
|---|---|
intercom_quick_reply_http_headers(access_token) | Authorization, Content-Type, Intercom-Version: Unstable. |
INTERCOM_PROACTIVE_QUICK_REPLY_DEFAULT_BODY | Default intro ("Need my expert help?") when you build body without ProactiveTriggerResult. |
build_intercom_quick_reply_reply_payload(admin_id=β¦, body=β¦, prompt_labels=[β¦]) | JSON for quick_reply with reply_options (β€ INTERCOM_PROACTIVE_PROMPTS_MAX = 3). |
normalize_intercom_quick_reply_labels | Strip / cap labels (used inside build_intercom_quick_reply_reply_payload). |
proactive_trigger_canonical_url_ping_pong(urls) | Low-level boolean predicate if you skip ProactiveTriggerRegistry. |
INTERCOM_REST_API_BASE (https://api.intercom.io).
Package: autoplay_sdk.proactive_triggers
Transport-agnostic detection (when to offer): ProactiveTriggerContext, ProactiveTriggerResult (trigger_id, body, optional reply_option_labels, metadata, interaction_timeout_s, cooldown_s β defaults 10s / 30s), ProactiveTriggerTimings, ProactiveTriggerEntity, ProactiveTriggerRegistry (evaluate_first / evaluate_all). Built-in trigger_id strings for shipped triggers are centralized in defaults (ProactiveTriggerIds, get_proactive_trigger_ids(), TRIGGER_ID_CANONICAL_URL_PING_PONG). CanonicalPingPongTrigger wraps proactive_trigger_canonical_url_ping_pong. default_proactive_trigger_registry() uses ProactiveTriggerEntity(CanonicalPingPongTrigger(), ProactiveTriggerTimings()). Default quick-reply intro copy for Intercom is DEFAULT_PROACTIVE_QUICK_REPLY_BODY in proactive_triggers.defaults, re-exported as INTERCOM_PROACTIVE_QUICK_REPLY_DEFAULT_BODY from integrations.intercom.
See the dedicated Proactive triggers page. Wire a firing ProactiveTriggerResult into build_intercom_quick_reply_reply_payload as in the recipe above.
Adding your own trigger
- Implement
ProactiveTrigger:trigger_idandevaluate(ctx) -> ProactiveTriggerResult | None. - Optionally wrap with
ProactiveTriggerEntity(inner, ProactiveTriggerTimings(...))for custom timeouts / cooldown. - Register on
ProactiveTriggerRegistryin priority order; callevaluate_firstin production.
Optional: connector LLM-generated button labels
POST β¦/intercom/proactive/{product_id} (admin key) returns up to three short strings for reply_options text when you want LLM-suggested labels alongside URL pingβpong (or other) proactive flows. Build the URL with intercom_connector_llm_prompt_labels_url, JSON body with build_connector_llm_prompt_labels_request_body. Product integration_config may include a proactive block β model it with IntercomProactivePolicyConfig.to_integration_config_fragment().
Connector endpoints (reference)
| HTTP | Role |
|---|---|
POST /chatbot-webhook/{product_id} | Primary path: Intercom signed webhooks for conversation.user.created / conversation.user.replied. |
POST /sessions/link | Optional: JSON body links session β conversation (reactive snippet). |
POST /intercom/proactive/{product_id} | Optional: LLM-generated proactive quick-reply labels (admin key). |
X-Hub-Signature-256 and your app client secret in product config.
Delivery stack in this repo
BaseChatbotWriter(Support agent writer) β pre-link buffer, post-link debounce, shared note body format (format_chatbot_note_header, numbered action lines, binning).IntercomChatbot(event connectorflows/chatbot/intercom.py) β subclass; implements Intercom REST admin notes (_post_note,_redact_part) and webhook payload parsing for linking.
AsyncAgentContextWriter as described on the support agent writer page.
Related
- Support agent writer β
BaseChatbotWritercontract and note format. - Agent context β summariser +
overwrite_with_summaryflow. - Agent session states β FSM gates when using the optional proactive connector endpoint with
conversation_id.